From: Julien Moutinho
Date: Thu, 18 Sep 2014 22:04:19 +0000 (+0200)
Subject: init
X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/operations/%27%20%20%20%20cl.base_img_url%20%20cl.license_img%5B%20lkey%20%5D.im%20%20%20%27?a=commitdiff_plain;h=1ab3343a95ed3ef4958d91dfbf49372dce8a092e;p=garradin.git
init
---
1ab3343a95ed3ef4958d91dfbf49372dce8a092e
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..395cdf0
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,7 @@
+
+ RedirectMatch 403 /include/
+ RedirectMatch 403 /cache/
+ RedirectMatch 403 /plugins/
+ RedirectMatch 403 /templates/
+ RedirectMatch 403 /*.sqlite
+
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..dba13ed
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+ .
diff --git a/README b/README
new file mode 100644
index 0000000..c11e982
--- /dev/null
+++ b/README
@@ -0,0 +1,33 @@
+Garradin - Gestionnaire d'association libre
+===========================================
+
+Inclus les bibliothèques suivantes :
+
+- Gibberish AES
+ https://github.com/mdp/gibberish-aes
+ Copyright : Mark Percival 2008 - http://markpercival.us
+ Licence : MIT
+
+- Countries - Liste des pays ISO 3166-1
+ Copyright : BohwaZ
+ Licence : Domaine public
+
+- Simple Diff PHP library
+ Copyright : BohwaZ 2009
+ Licence : GNU GPL v3
+
+- Garbage2xhtml - HTML cleaner
+ Copyright : BohwaZ 2006-2011
+ Licence : GNU AGPL v3
+
+- miniSkel - SPIP-like templates
+ Copyright : BohwaZ 2007-2012
+ Licence : GNU GPL v3
+
+- Passphrase - a PHP library to generate passphrases
+ Copyright : BohwaZ 2011-2012
+ Licence : WTFPL
+
+- Template_Lite
+ Copyright : 2003,2004,2005 by Paul Lockaby, 2005,2006 Mark Dickenson, 2005-2012 BohwaZ
+ Licence : GNU GPL v2.1
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..b616048
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.6.2
diff --git a/config.dist.php b/config.dist.php
new file mode 100644
index 0000000..15ef961
--- /dev/null
+++ b/config.dist.php
@@ -0,0 +1,63 @@
+get('frequence_sauvegardes') && $config->get('nombre_sauvegardes'))
+{
+ $s = new Sauvegarde;
+ $s->auto();
+}
+
+
+// Exécution des rappels automatiques
+$rappels = new Rappels;
+
+if ($rappels->countAll())
+{
+ $rappels->sendPending();
+}
+
+// Nettoyage du cache statique
+Static_Cache::clean();
\ No newline at end of file
diff --git a/include/class.champs_membres.php b/include/class.champs_membres.php
new file mode 100644
index 0000000..9c07ae5
--- /dev/null
+++ b/include/class.champs_membres.php
@@ -0,0 +1,470 @@
+ 'Adresse E-Mail',
+ 'url' => 'Adresse URL',
+ 'checkbox' => 'Case à cocher',
+ 'date' => 'Date',
+ 'datetime' => 'Date et heure',
+ //'file' => 'Fichier',
+ 'password' => 'Mot de passe',
+ 'number' => 'Numéro',
+ 'tel' => 'Numéro de téléphone',
+ 'select' => 'Sélecteur à choix unique',
+ 'multiple' => 'Sélecteur à choix multiple',
+ 'country' => 'Sélecteur de pays',
+ 'text' => 'Texte',
+ 'textarea' => 'Texte multi-lignes',
+ ];
+
+ protected $text_types = [
+ 'email',
+ 'text',
+ 'select',
+ 'textarea',
+ 'url',
+ 'password',
+ 'country'
+ ];
+
+ protected $config_fields = [
+ 'type',
+ 'title',
+ 'help',
+ 'editable',
+ 'list_row',
+ 'mandatory',
+ 'private',
+ 'options'
+ ];
+
+ static protected $presets = null;
+
+ public function __toString()
+ {
+ return utils::write_ini_string($this->champs);
+ }
+
+ public function toString()
+ {
+ return utils::write_ini_string($this->champs);
+ }
+
+ static public function importInstall()
+ {
+ $champs = parse_ini_file(ROOT . '/include/data/champs_membres.ini', true);
+ $champs = array_filter($champs, function ($row) { return !empty($row['install']); });
+ return new Champs_Membres($champs);
+ }
+
+ static public function importPresets()
+ {
+ if (is_null(self::$presets))
+ {
+ self::$presets = parse_ini_file(ROOT . '/include/data/champs_membres.ini', true);
+ }
+
+ return self::$presets;
+ }
+
+ static public function listUnusedPresets(Champs_Membres $champs)
+ {
+ return array_diff_key(self::importPresets(), $champs->getAll());
+ }
+
+ public function __construct($champs)
+ {
+ if ($champs instanceOf Champs_Membres)
+ {
+ $this->champs = $champs->getAll();
+ }
+ elseif (is_array($champs))
+ {
+ foreach ($champs as $key=>&$config)
+ {
+ $this->_checkField($key, $config);
+ }
+
+ $this->champs = $champs;
+ }
+ else
+ {
+ $champs = parse_ini_string((string)$champs, true);
+
+ foreach ($champs as $key=>&$config)
+ {
+ $this->_checkField($key, $config);
+ }
+
+ $this->champs = $champs;
+ }
+ }
+
+ public function getTypes()
+ {
+ return $this->types;
+ }
+
+ public function get($champ, $key = null)
+ {
+ if ($champ == 'id')
+ {
+ return ['title' => 'Numéro unique', 'type' => 'number'];
+ }
+
+ if (!array_key_exists($champ, $this->champs))
+ return null;
+
+ if ($key !== null)
+ {
+ if (array_key_exists($key, $this->champs[$champ]))
+ return $this->champs[$champ][$key];
+ else
+ return null;
+ }
+
+ return $this->champs[$champ];
+ }
+
+ public function isText($champ)
+ {
+ if (!array_key_exists($champ, $this->champs))
+ return null;
+
+ if (in_array($this->champs[$champ]['type'], $this->text_types))
+ return true;
+ else
+ return false;
+ }
+
+ public function getAll()
+ {
+ $this->champs['passe']['title'] = 'Mot de passe';
+ return $this->champs;
+ }
+
+ public function getList()
+ {
+ $champs = $this->champs;
+ unset($champs['passe']);
+ return $champs;
+ }
+
+ public function getFirst()
+ {
+ reset($this->champs);
+ return key($this->champs);
+ }
+
+ public function getListedFields()
+ {
+ $champs = $this->champs;
+
+ $champs = array_filter($champs, function ($a) {
+ return empty($a['list_row']) ? false : true;
+ });
+
+ uasort($champs, function ($a, $b) {
+ if ($a['list_row'] == $b['list_row'])
+ return 0;
+
+ return ($a['list_row'] > $b['list_row']) ? 1 : -1;
+ });
+
+ return $champs;
+ }
+
+ /**
+ * Vérifie la cohérence et la présence des bons éléments pour un champ
+ * @param string $name Nom du champ
+ * @param array $config Configuration du champ
+ * @return boolean true
+ */
+ protected function _checkField($name, &$config)
+ {
+ if (!preg_match('!^\w+(_\w+)*$!', $name))
+ {
+ throw new UserException('Le nom du champ est invalide.');
+ }
+
+ foreach ($config as $key=>&$value)
+ {
+ // Champ install non pris en compte
+ if ($key == 'install')
+ {
+ unset($config[$key]);
+ continue;
+ }
+
+ if (!in_array($key, $this->config_fields))
+ {
+ throw new \BadMethodCallException('Champ '.$key.' non valide.');
+ }
+
+ if ($key == 'editable' || $key == 'private' || $key == 'mandatory')
+ {
+ $value = (bool) (int) $value;
+ }
+ elseif ($key == 'list_row')
+ {
+ $value = (int) $value;
+ }
+ elseif ($key == 'help' || $key == 'title')
+ {
+ $value = trim((string) $value);
+ }
+ elseif ($key == 'options')
+ {
+ $value = (array) $value;
+
+ foreach ($value as $option_key=>$option_value)
+ {
+ if (trim($option_value) == '')
+ {
+ unset($value[$option_key]);
+ }
+ }
+ }
+ }
+
+ if (empty($config['title']) && $name != 'passe')
+ {
+ throw new UserException('Champ "'.$name.'" : Le titre est obligatoire.');
+ }
+
+ if (empty($config['type']) || !array_key_exists($config['type'], $this->types))
+ {
+ throw new UserException('Champ "'.$name.'" : Le type est vide ou non valide.');
+ }
+
+ if ($name == 'email' && $config['type'] != 'email')
+ {
+ throw new UserException('Le champ email ne peut être d\'un type différent de email.');
+ }
+
+ if ($name == 'passe' && $config['type'] != 'password')
+ {
+ throw new UserException('Le champ mot de passe ne peut être d\'un type différent de mot de passe.');
+ }
+
+ if (($config['type'] == 'multiple' || $config['type'] == 'select') && empty($config['options']))
+ {
+ throw new UserException('Le champ "'.$name.'" nécessite de comporter au moins une option possible.');
+ }
+
+ if (!array_key_exists('editable', $config))
+ {
+ $config['editable'] = false;
+ }
+
+ if (!array_key_exists('mandatory', $config))
+ {
+ $config['mandatory'] = false;
+ }
+
+ if (!array_key_exists('private', $config))
+ {
+ $config['private'] = false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Ajouter un nouveau champ
+ * @param string $name Nom du champ
+ * @param array $config Configuration du champ
+ * @return boolean true
+ */
+ public function add($name, $config)
+ {
+ if (!preg_match('!^[a-z0-9]+(_[a-z0-9]+)*$!', $name))
+ {
+ throw new UserException('Le nom du champ est invalide : ne sont acceptés que des lettres minuscules et chiffres.');
+ }
+
+ $this->_checkField($name, $config);
+
+ $this->champs[$name] = $config;
+
+ return true;
+ }
+
+ /**
+ * Modifie un champ particulier
+ * @param string $champ Nom du champ
+ * @param string $key Nom de la clé à modifier
+ * @param mixed $value Valeur à affecter
+ * @return boolean true
+ */
+ public function set($champ, $key, $value)
+ {
+ if (!isset($this->champs[$champ]))
+ {
+ throw new \LogicException('Champ "'.$champ.'" inconnu.');
+ }
+
+ // Vérification
+ $config = $this->champs[$champ];
+ $config[$key] = $value;
+ $this->_checkField($champ, $config);
+
+ $this->champs[$champ] = $config;
+ return true;
+ }
+
+ /**
+ * Modifie les champs en interne en vérifiant que tout va bien
+ * @param array $champs Liste des champs
+ * @return boolean true
+ */
+ public function setAll($champs)
+ {
+ if (!array_key_exists('email', $champs))
+ {
+ throw new UserException('Le champ E-Mail ne peut être supprimé des fiches membres.');
+ }
+
+ if (!array_key_exists('passe', $champs))
+ {
+ throw new UserException('Le champ Mot de passe ne peut être supprimé des fiches membres.');
+ }
+
+ $config = Config::getInstance();
+
+ if (!array_key_exists($config->get('champ_identite'), $champs))
+ {
+ throw new UserException('Le champ '.$config->get('champ_identite')
+ .' est défini comme identité des membres et ne peut donc être supprimé des fiches membres.');
+ }
+
+ if (!array_key_exists($config->get('champ_identifiant'), $champs))
+ {
+ throw new UserException('Le champ '.$config->get('champ_identifiant')
+ .' est défini comme identifiant à la connexion et ne peut donc être supprimé des fiches membres.');
+ }
+
+ foreach ($champs as $name=>&$config)
+ {
+ $this->_checkField($name, $config);
+ }
+
+ $this->champs = $champs;
+
+ return true;
+ }
+
+ /**
+ * Enregistre les changements de champs en base de données
+ * @param boolean $enable_copy Recopier les anciennes champs dans les nouveaux ?
+ * @return boolean true
+ */
+ public function save($enable_copy = true)
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ // Champs à créer
+ $create = [
+ 'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement',
+ 'id_categorie INTEGER NOT NULL, -- Numéro de catégorie',
+ 'date_connexion TEXT NULL, -- Date de dernière connexion',
+ 'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE, -- Date d\'inscription',
+ ];
+
+ $create_keys = [
+ 'FOREIGN KEY (id_categorie) REFERENCES membres_categories (id)'
+ ];
+
+ // Champs à recopier
+ $copy = [
+ 'id',
+ 'id_categorie',
+ 'date_connexion',
+ 'date_inscription',
+ ];
+
+ $anciens_champs = $config->get('champs_membres');
+ $anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll();
+
+ foreach ($this->champs as $key=>$cfg)
+ {
+ if ($cfg['type'] == 'number')
+ $type = 'FLOAT';
+ elseif ($cfg['type'] == 'multiple' || $cfg['type'] == 'checkbox')
+ $type = 'INTEGER';
+ elseif ($cfg['type'] == 'file')
+ $type = 'BLOB';
+ else
+ $type = 'TEXT';
+
+ $line = $key . ' ' . $type . ',';
+
+ if (!empty($cfg['title']))
+ {
+ $line .= ' -- ' . str_replace(["\n", "\r"], '', $cfg['title']);
+ }
+
+ $create[] = $line;
+
+ if (array_key_exists($key, $anciens_champs))
+ {
+ $copy[] = $key;
+ }
+ }
+
+ $create = array_merge($create, $create_keys);
+
+ $create = 'CREATE TABLE membres_tmp (' . "\n\t" . implode("\n\t", $create) . "\n);";
+ $copy = 'INSERT INTO membres_tmp (' . implode(', ', $copy) . ') SELECT ' . implode(', ', $copy) . ' FROM membres;';
+
+ $db->exec('PRAGMA foreign_keys = OFF;');
+ $db->exec('BEGIN;');
+ $db->exec($create);
+
+ if ($enable_copy) {
+ $db->exec($copy);
+ }
+
+ $db->exec('DROP TABLE IF EXISTS membres;');
+ $db->exec('ALTER TABLE membres_tmp RENAME TO membres;');
+ $db->exec('CREATE INDEX membres_id_categorie ON membres (id_categorie);'); // Index
+
+ if ($config->get('champ_identifiant'))
+ {
+ // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique
+ $db->exec('UPDATE membres SET '.$config->get('champ_identifiant').' = NULL
+ WHERE '.$config->get('champ_identifiant').' = "";');
+
+ // Création de l'index unique
+ $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$config->get('champ_identifiant').');');
+ }
+
+ // Création des index pour les champs affichés dans la liste des membres
+ $listed_fields = array_keys($this->getListedFields());
+ foreach ($listed_fields as $field)
+ {
+ if ($field === $config->get('champ_identifiant'))
+ {
+ // Il y a déjà un index
+ continue;
+ }
+
+ $db->exec('CREATE INDEX membres_liste_' . $field . ' ON membres (' . $field . ');');
+ }
+
+ $db->exec('END;');
+ $db->exec('PRAGMA foreign_keys = ON;');
+
+ $config->set('champs_membres', $this);
+ $config->save();
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/include/class.compta_categories.php b/include/class.compta_categories.php
new file mode 100644
index 0000000..0798be2
--- /dev/null
+++ b/include/class.compta_categories.php
@@ -0,0 +1,126 @@
+exec(file_get_contents(ROOT . '/include/data/categories_comptables.sql'));
+ }
+
+ public function add($data)
+ {
+ $this->_checkFields($data);
+
+ $db = DB::getInstance();
+
+ if (empty($data['compte']) || !trim($data['compte']))
+ {
+ throw new UserException('Le compte associé ne peut rester vide.');
+ }
+
+ $data['compte'] = trim($data['compte']);
+
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['compte']))
+ {
+ throw new UserException('Le compte associé n\'existe pas.');
+ }
+
+ if (!isset($data['type']) ||
+ ($data['type'] != self::DEPENSES && $data['type'] != self::RECETTES))
+ {
+ // Catégories "autres" pas possibles pour le moment
+ throw new UserException('Type de catégorie inconnu.');
+ }
+
+ $db->simpleInsert('compta_categories', [
+ 'intitule' => $data['intitule'],
+ 'description'=> $data['description'],
+ 'compte' => $data['compte'],
+ 'type' => (int)$data['type'],
+ ]);
+
+ return $db->lastInsertRowId();
+ }
+
+ public function edit($id, $data)
+ {
+ $this->_checkFields($data);
+
+ $db = DB::getInstance();
+
+ $db->simpleUpdate('compta_categories',
+ [
+ 'intitule' => $data['intitule'],
+ 'description'=> $data['description'],
+ ],
+ 'id = \''.$db->escapeString(trim($id)).'\'');
+
+ return true;
+ }
+
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+
+ // Ne pas supprimer une catégorie qui est utilisée !
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_categorie = ? LIMIT 1;', false, $id))
+ {
+ throw new UserException('Cette catégorie ne peut être supprimée car des opérations comptables y sont liées.');
+ }
+
+ $db->simpleExec('DELETE FROM compta_categories WHERE id = ?;', $id);
+
+ return true;
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT * FROM compta_categories WHERE id = ?;', true, (int)$id);
+ }
+
+ public function getList($type = null)
+ {
+ $db = DB::getInstance();
+ $type = is_null($type) ? '' : 'cat.type = '.(int)$type;
+ return $db->simpleStatementFetchAssocKey('
+ SELECT cat.id, cat.*, cc.libelle AS compte_libelle
+ FROM compta_categories AS cat INNER JOIN compta_comptes AS cc
+ ON cc.id = cat.compte
+ WHERE '.$type.' ORDER BY cat.intitule;', SQLITE3_ASSOC);
+ }
+
+ public function listMoyensPaiement()
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetchAssocKey('SELECT code, nom FROM compta_moyens_paiement ORDER BY nom COLLATE NOCASE;');
+ }
+
+ public function getMoyenPaiement($code)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT nom FROM compta_moyens_paiement WHERE code = ?;', false, $code);
+ }
+
+ protected function _checkFields(&$data)
+ {
+ if (empty($data['intitule']) || !trim($data['intitule']))
+ {
+ throw new UserException('L\'intitulé ne peut rester vide.');
+ }
+
+ $data['intitule'] = trim($data['intitule']);
+ $data['description'] = isset($data['description']) ? trim($data['description']) : '';
+
+ return true;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.compta_comptes.php b/include/class.compta_comptes.php
new file mode 100644
index 0000000..02021a2
--- /dev/null
+++ b/include/class.compta_comptes.php
@@ -0,0 +1,325 @@
+exec('BEGIN;');
+ $ids = [];
+
+ foreach ($plan as $id=>$compte)
+ {
+ $ids[] = $id;
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $id))
+ {
+ $db->simpleUpdate('compta_comptes', [
+ 'parent' => $compte['parent'],
+ 'libelle' => $compte['nom'],
+ 'position' => $compte['position'],
+ 'plan_comptable' => 1,
+ ], 'id = \''.$db->escapeString($id).'\'');
+ }
+ else
+ {
+ $db->simpleInsert('compta_comptes', [
+ 'id' => $id,
+ 'parent' => $compte['parent'],
+ 'libelle' => $compte['nom'],
+ 'position' => $compte['position'],
+ 'plan_comptable' => 1,
+ ]);
+ }
+ }
+
+ $db->exec('DELETE FROM compta_comptes WHERE id NOT IN(\''.implode('\', \'', $ids).'\') AND plan_comptable = 1;');
+
+ $db->exec('END;');
+
+ return true;
+ }
+
+ public function add($data)
+ {
+ $this->_checkFields($data, true);
+
+ $db = DB::getInstance();
+
+ if (empty($data['id']))
+ {
+ $new_id = $data['parent'];
+ $nb_sous_comptes = $db->simpleQuerySingle('SELECT COUNT(*) FROM compta_comptes WHERE parent = ?;', false, $new_id);
+
+ // Pas plus de 26 sous-comptes par compte, parce que l'alphabet s'arrête à 26 lettres
+ if ($nb_sous_comptes >= 26)
+ {
+ throw new UserException('Nombre de sous-comptes maximal atteint pour ce compte parent-ci.');
+ }
+
+ $new_id .= chr(65+(int)$nb_sous_comptes);
+ }
+ else
+ {
+ $new_id = $data['id'];
+ }
+
+ if (isset($data['position']))
+ {
+ $position = (int) $data['position'];
+ }
+ else
+ {
+ $position = $db->simpleQuerySingle('SELECT position FROM compta_comptes WHERE id = ?;', false, $data['parent']);
+ }
+
+ $db->simpleInsert('compta_comptes', [
+ 'id' => $new_id,
+ 'libelle' => trim($data['libelle']),
+ 'parent' => $data['parent'],
+ 'plan_comptable' => 0,
+ 'position' => (int)$position,
+ ]);
+
+ return $new_id;
+ }
+
+ public function edit($id, $data)
+ {
+ $db = DB::getInstance();
+
+ // Vérification que l'on peut éditer ce compte
+ if ($db->simpleQuerySingle('SELECT plan_comptable FROM compta_comptes WHERE id = ?;', false, $id))
+ {
+ throw new UserException('Ce compte fait partie du plan comptable et n\'est pas modifiable.');
+ }
+
+ if (isset($data['position']) && empty($data['position']))
+ {
+ throw new UserException('Aucune position du compte n\'a été indiquée.');
+ }
+
+ $this->_checkFields($data);
+
+ $update = [
+ 'libelle' => trim($data['libelle']),
+ ];
+
+ if (isset($data['position']))
+ {
+ $update['position'] = (int) trim($data['position']);
+ }
+
+ $db->simpleUpdate('compta_comptes', $update, 'id = \''.$db->escapeString(trim($id)).'\'');
+
+ return true;
+ }
+
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+
+ // Ne pas supprimer un compte qui est utilisé !
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE compte_debit = ? OR compte_debit = ? LIMIT 1;', false, $id, $id))
+ {
+ throw new UserException('Ce compte ne peut être supprimé car des opérations comptables y sont liées.');
+ }
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ? LIMIT 1;', false, $id))
+ {
+ throw new UserException('Ce compte ne peut être supprimé car il est lié à un compte bancaire.');
+ }
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id))
+ {
+ throw new UserException('Ce compte ne peut être supprimé car des catégories y sont liées.');
+ }
+
+ $db->simpleExec('DELETE FROM compta_comptes WHERE id = ?;', trim($id));
+
+ return true;
+ }
+
+ /**
+ * Peut-on supprimer ce compte ? (OUI s'il n'a pas d'écriture liée)
+ * @param string $id Numéro du compte
+ * @return boolean TRUE si le compte n'a pas d'écriture liée
+ */
+ public function canDelete($id)
+ {
+ $db = DB::getInstance();
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal
+ WHERE compte_debit = ? OR compte_debit = ? LIMIT 1;', false, $id, $id))
+ {
+ return false;
+ }
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Peut-on désactiver ce compte ? (OUI s'il n'a pas d'écriture liée dans l'exercice courant)
+ * @param string $id Numéro du compte
+ * @return boolean TRUE si le compte n'a pas d'écriture liée dans l'exercice courant
+ */
+ public function canDisable($id)
+ {
+ $db = DB::getInstance();
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal
+ WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1)
+ AND (compte_debit = ? OR compte_debit = ?) LIMIT 1;', false, $id, $id))
+ {
+ return false;
+ }
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Désactiver un compte
+ * Le compte ne sera plus utilisable pour les écritures ou les catégories mais restera en base de données
+ * @param string $id Numéro du compte
+ * @return boolean TRUE si la désactivation a fonctionné, une exception utilisateur si
+ * la désactivation n'est pas possible.
+ */
+ public function disable($id)
+ {
+ $db = DB::getInstance();
+
+ // Ne pas désactiver un compte utilisé dans l'exercice courant
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal
+ WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1)
+ AND (compte_debit = ? OR compte_debit = ?) LIMIT 1;', false, $id, $id))
+ {
+ throw new UserException('Ce compte ne peut être désactivé car des écritures y sont liées sur l\'exercice courant. '
+ . 'Il faut supprimer ou ré-attribuer ces écritures avant de pouvoir supprimer le compte.');
+ }
+
+ // Ne pas désactiver un compte utilisé pour une catégorie
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id))
+ {
+ throw new UserException('Ce compte ne peut être désactivé car des catégories y sont liées.');
+ }
+
+ return $db->simpleUpdate('compta_comptes', ['desactive' => 1], 'id = \''.$db->escapeString(trim($id)).'\'');
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT * FROM compta_comptes WHERE id = ?;', true, trim($id));
+ }
+
+ public function getList($parent = 0)
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetchAssocKey('SELECT id, * FROM compta_comptes WHERE parent = ? ORDER BY id;', SQLITE3_ASSOC, $parent);
+ }
+
+ public function getListAll($parent = 0)
+ {
+ $db = DB::getInstance();
+ return $db->queryFetchAssoc('SELECT id, libelle FROM compta_comptes ORDER BY id;');
+ }
+
+ public function listTree($parent = 0, $include_children = true)
+ {
+ $db = DB::getInstance();
+
+ if ($include_children)
+ {
+ $parent = $parent ? 'WHERE parent LIKE \''.$db->escapeString($parent).'%\' ' : '';
+ }
+ else
+ {
+ $parent = $parent ? 'WHERE parent = \''.$db->escapeString($parent).'\' ' : 'WHERE parent = 0';
+ }
+
+ return $db->simpleStatementFetch('SELECT * FROM compta_comptes '.$parent.' ORDER BY id;');
+ }
+
+ protected function _checkFields(&$data, $force_parent_check = false)
+ {
+ $db = DB::getInstance();
+
+ if (empty($data['libelle']) || !trim($data['libelle']))
+ {
+ throw new UserException('Le libellé ne peut rester vide.');
+ }
+
+ $data['libelle'] = trim($data['libelle']);
+
+ if (isset($data['id']))
+ {
+ $force_parent_check = true;
+ $data['id'] = trim($data['id']);
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['id']))
+ {
+ throw new UserException('Le compte numéro '.$data['id'].' existe déjà .');
+ }
+ }
+
+ if (isset($data['parent']) || $force_parent_check)
+ {
+ if (empty($data['parent']) && !trim($data['parent']))
+ {
+ throw new UserException('Le compte ne peut pas ne pas avoir de compte parent.');
+ }
+
+ if (!($id = $db->simpleQuerySingle('SELECT id FROM compta_comptes WHERE id = ?;', false, $data['parent'])))
+ {
+ throw new UserException('Le compte parent indiqué n\'existe pas.');
+ }
+
+ $data['parent'] = trim($id);
+ }
+
+ if (isset($data['id']))
+ {
+ if (strncmp($data['id'], $data['parent'], strlen($data['parent'])) !== 0)
+ {
+ throw new UserException('Le compte '.$data['id'].' n\'est pas un sous-compte de '.$data['parent'].'.');
+ }
+ }
+
+ return true;
+ }
+
+ public function getPositions()
+ {
+ return [
+ self::ACTIF => 'Actif',
+ self::PASSIF => 'Passif',
+ self::ACTIF | self::PASSIF => 'Actif ou passif (déterminé automatiquement au bilan selon le solde du compte)',
+ self::CHARGE => 'Charge',
+ self::PRODUIT => 'Produit',
+ self::CHARGE | self::PRODUIT => 'Charge et produit',
+ ];
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.compta_comptes_bancaires.php b/include/class.compta_comptes_bancaires.php
new file mode 100644
index 0000000..1ac1213
--- /dev/null
+++ b/include/class.compta_comptes_bancaires.php
@@ -0,0 +1,164 @@
+_checkBankFields($data);
+
+ $new_id = parent::add($data);
+
+ $db->simpleInsert('compta_comptes_bancaires', [
+ 'id' => $new_id,
+ 'banque' => $data['banque'],
+ 'iban' => $data['iban'],
+ 'bic' => $data['bic'],
+ ]);
+
+ return $new_id;
+ }
+
+ public function edit($id, $data)
+ {
+ $db = DB::getInstance();
+
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ?;', false, $id))
+ {
+ throw new UserException('Ce compte n\'est pas un compte bancaire.');
+ }
+
+ $this->_checkBankFields($data);
+ $result = parent::edit($id, $data);
+
+ if (!$result)
+ {
+ return $result;
+ }
+
+ $db->simpleUpdate('compta_comptes_bancaires', [
+ 'banque' => $data['banque'],
+ 'iban' => $data['iban'],
+ 'bic' => $data['bic'],
+ ], 'id = \''.$db->escapeString(trim($id)).'\'');
+
+ return true;
+ }
+
+ /**
+ * Supprime un compte bancaire
+ * La suppression sera refusée si le compte est utilisé dans l'exercice en cours
+ * ou dans une catégorie.
+ * Le compte bancaire sera supprimé et le compte au plan comptable seulement désactivé
+ * si le compte est utilisé dans un exercice précédent.
+ *
+ * La désactivation d'un compte fait qu'il n'est plus utilisable dans l'exercice courant
+ * ou les exercices suivants, mais il est possible de le réactiver.
+ * @param string $id Numéro du compte
+ * @return boolean TRUE si la suppression ou désactivation a été effectuée, une exception ou FALSE sinon
+ */
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ?;', false, trim($id)))
+ {
+ throw new UserException('Ce compte n\'est pas un compte bancaire.');
+ }
+
+ // Ne pas supprimer/désactiver un compte qui est utilisé dans l'exercice courant
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal
+ WHERE id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1)
+ AND (compte_debit = ? OR compte_debit = ?) LIMIT 1;', false, $id, $id))
+ {
+ throw new UserException('Ce compte ne peut être supprimé car des écritures y sont liées sur l\'exercice courant. '
+ . 'Il faut supprimer ou ré-attribuer ces écritures avant de pouvoir supprimer le compte.');
+ }
+
+ // Il n'est pas possible de supprimer ou désactiver un compte qui est lié à des catégories
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE compte = ? LIMIT 1;', false, $id))
+ {
+ throw new UserException('Ce compte ne peut être supprimé car des catégories y sont liées. '
+ . 'Merci de supprimer ou modifier les catégories liées avant de le supprimer.');
+ }
+
+ $db->simpleExec('DELETE FROM compta_comptes_bancaires WHERE id = ?;', trim($id));
+
+ try {
+ $return = parent::delete($id);
+ }
+ catch (UserException $e) {
+ // Impossible de supprimer car des opérations y sont encore liées
+ // sur les exercices précédents, alors on le désactive
+ $return = parent::disable($id);
+ }
+
+ return $return;
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT * FROM compta_comptes AS c
+ INNER JOIN compta_comptes_bancaires AS cc
+ ON c.id = cc.id
+ WHERE c.id = ?;', true, $id);
+ }
+
+ public function getList($parent = false)
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetchAssocKey('SELECT c.id AS id, * FROM compta_comptes AS c
+ INNER JOIN compta_comptes_bancaires AS cc ON c.id = cc.id
+ WHERE c.parent = '.self::NUMERO_PARENT_COMPTES.' ORDER BY c.id;');
+ }
+
+ protected function _checkBankFields(&$data)
+ {
+ if (empty($data['banque']) || !trim($data['banque']))
+ {
+ throw new UserException('Le nom de la banque ne peut rester vide.');
+ }
+
+ if (empty($data['bic']))
+ {
+ $data['bic'] = '';
+ }
+ else
+ {
+ $data['bic'] = trim(strtoupper($data['bic']));
+ $data['bic'] = preg_replace('![^\dA-Z]!', '', $data['bic']);
+
+ if (!utils::checkBIC($data['bic']))
+ {
+ throw new UserException('Code BIC/SWIFT invalide.');
+ }
+ }
+
+ if (empty($data['iban']))
+ {
+ $data['iban'] = '';
+ }
+ else
+ {
+ $data['iban'] = trim(strtoupper($data['iban']));
+ $data['iban'] = preg_replace('![^\dA-Z]!', '', $data['iban']);
+
+ if (!utils::checkIBAN($data['iban']))
+ {
+ throw new UserException('Code IBAN invalide.');
+ }
+ }
+
+ return true;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.compta_exercices.php b/include/class.compta_exercices.php
new file mode 100644
index 0000000..02edb14
--- /dev/null
+++ b/include/class.compta_exercices.php
@@ -0,0 +1,569 @@
+_checkFields($data);
+
+ $db = DB::getInstance();
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE
+ (debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin);', false,
+ ['debut' => $data['debut'], 'fin' => $data['fin']]))
+ {
+ throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.');
+ }
+
+ if ($db->querySingle('SELECT 1 FROM compta_exercices WHERE cloture = 0;'))
+ {
+ throw new UserException('Il n\'est pas possible de créer un nouvel exercice tant qu\'il existe un exercice non-clôturé.');
+ }
+
+ $db->simpleInsert('compta_exercices', [
+ 'libelle' => trim($data['libelle']),
+ 'debut' => $data['debut'],
+ 'fin' => $data['fin'],
+ ]);
+
+ return $db->lastInsertRowId();
+ }
+
+ public function edit($id, $data)
+ {
+ $db = DB::getInstance();
+
+ $this->_checkFields($data);
+
+ // Evitons que les exercices se croisent
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE id != :id AND
+ ((debut <= :debut AND fin >= :debut) OR (debut <= :fin AND fin >= :fin));', false,
+ ['debut' => $data['debut'], 'fin' => $data['fin'], 'id' => (int) $id]))
+ {
+ throw new UserException('La date de début ou de fin se recoupe avec un autre exercice.');
+ }
+
+ // On vérifie qu'on ne va pas mettre des opérations en dehors de tout exercice
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ?
+ AND date < ? LIMIT 1;', false, (int)$id, $data['debut']))
+ {
+ throw new UserException('Des opérations de cet exercice ont une date antérieure à la date de début de l\'exercice.');
+ }
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ?
+ AND date > ? LIMIT 1;', false, (int)$id, $data['fin']))
+ {
+ throw new UserException('Des opérations de cet exercice ont une date postérieure à la date de fin de l\'exercice.');
+ }
+
+ $db->simpleUpdate('compta_exercices', [
+ 'libelle' => trim($data['libelle']),
+ 'debut' => $data['debut'],
+ 'fin' => $data['fin'],
+ ], 'id = \''.(int)$id.'\'');
+
+ return true;
+ }
+
+ /**
+ * Clôturer un exercice et en ouvrir un nouveau
+ * Le report à nouveau n'est pas effectué automatiquement par cette fonction, voir doReports pour ça.
+ * @param integer $id ID de l'exercice à clôturer
+ * @param string $end Date de clôture de l'exercice au format Y-m-d
+ * @return integer L'ID du nouvel exercice créé
+ */
+ public function close($id, $end)
+ {
+ $db = DB::getInstance();
+
+ if (!utils::checkDate($end))
+ {
+ throw new UserException('Date de fin vide ou invalide.');
+ }
+
+ $db->exec('BEGIN;');
+
+ // Clôture de l'exercice
+ $db->simpleUpdate('compta_exercices', [
+ 'cloture' => 1,
+ 'fin' => $end,
+ ], 'id = \''.(int)$id.'\'');
+
+ // Date de début du nouvel exercice : lendemain de la clôture du précédent exercice
+ $new_begin = utils::modifyDate($end, '+1 day');
+
+ // Date de fin du nouvel exercice : un an moins un jour après l'ouverture
+ $new_end = utils::modifyDate($new_begin, '+1 year -1 day');
+
+ // Enfin sauf s'il existe déjà des opérations après cette date, auquel cas la date de fin
+ // est fixée à la date de la dernière opération, ceci pour ne pas avoir d'opération
+ // orpheline d'exercice
+ $last = $db->simpleQuerySingle('SELECT date FROM compta_journal WHERE id_exercice = ? AND date >= ? ORDER BY date DESC LIMIT 1;', false, $id, $new_end);
+ $new_end = $last ?: $new_end;
+
+ // Création du nouvel exercice
+ $new_id = $this->add([
+ 'debut' => $new_begin,
+ 'fin' => $new_end,
+ 'libelle' => 'Nouvel exercice'
+ ]);
+
+ // Ré-attribution des opérations de l'exercice à clôturer qui ne sont pas dans son
+ // intervale au nouvel exercice
+ $db->simpleExec('UPDATE compta_journal SET id_exercice = ? WHERE id_exercice = ? AND date >= ?;',
+ $new_id, $id, $new_begin);
+
+ $db->exec('END;');
+
+ return $new_id;
+ }
+
+ /**
+ * Créer les reports à nouveau issus de l'exercice $old_id dans le nouvel exercice courant
+ * @param integer $old_id ID de l'ancien exercice
+ * @param integer $new_id ID du nouvel exercice
+ * @param string $date Date Y-m-d donnée aux opérations créées
+ * @return boolean true si succès
+ */
+ public function doReports($old_id, $date)
+ {
+ $db = DB::getInstance();
+
+ $db->exec('BEGIN;');
+
+ $this->solderResultat($old_id, $date);
+
+ $report_crediteur = 110;
+ $report_debiteur = 119;
+
+ // Récupérer chacun des comptes de bilan et leurs soldes (uniquement les classes 1 à 5)
+ $statement = $db->simpleStatement('SELECT compta_comptes.id AS compte, compta_comptes.position AS position,
+ COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit = compta_comptes.id AND id_exercice = :id), 0)
+ - COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit = compta_comptes.id AND id_exercice = :id), 0) AS solde
+ FROM compta_comptes
+ INNER JOIN compta_journal ON compta_comptes.id = compta_journal.compte_debit
+ OR compta_comptes.id = compta_journal.compte_credit
+ WHERE id_exercice = :id AND solde != 0 AND CAST(substr(compta_comptes.id, 1, 1) AS INTEGER) <= 5
+ GROUP BY compta_comptes.id;', ['id' => $old_id]);
+
+ $diff = 0;
+ $journal = new Compta_Journal;
+
+ while ($row = $statement->fetchArray(SQLITE3_ASSOC))
+ {
+ $solde = ($row['position'] & Compta_Comptes::ACTIF) ? abs($row['solde']) : -abs($row['solde']);
+ $solde = round($solde, 2);
+
+ $diff += $solde;
+
+ if (empty($solde))
+ {
+ continue;
+ }
+
+ // Chaque solde de compte est reporté dans le nouvel exercice
+ $journal->add([
+ 'libelle' => 'Report à nouveau',
+ 'date' => $date,
+ 'montant' => abs($solde),
+ 'compte_debit' => ($solde < 0 ? NULL : $row['compte']),
+ 'compte_credit' => ($solde > 0 ? NULL : $row['compte']),
+ 'remarques' => 'Report de solde créé automatiquement à la clôture de l\'exercice précédent',
+ ]);
+ }
+
+ // FIXME utiliser $diff pour équilibrer
+
+ $db->exec('END;');
+
+ return true;
+ }
+
+ /**
+ * Solder les comptes de charge et de produits de l'exercice N
+ * et les inscrire au résultat de l'exercice N+1
+ * @param integer $exercice ID de l'exercice à solder
+ * @param string $date Date de début de l'exercice Y-m-d
+ * @return boolean true en cas de succès
+ */
+ public function solderResultat($exercice, $date)
+ {
+ $db = DB::getInstance();
+
+ $resultat_excedent = 120;
+ $resultat_debiteur = 129;
+
+ $resultat = $this->getCompteResultat($exercice);
+ $resultat = $resultat['resultat'];
+
+ if ($resultat != 0)
+ {
+ $journal = new Compta_Journal;
+ $journal->add([
+ 'libelle' => 'Résultat de l\'exercice précédent',
+ 'date' => $date,
+ 'montant' => abs($resultat),
+ 'compte_debit' => $resultat < 0 ? 129 : NULL,
+ 'compte_credit' => $resultat > 0 ? 120 : NULL,
+ ]);
+ }
+
+ return true;
+ }
+
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+
+ // Ne pas supprimer un compte qui est utilisé !
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_exercice = ? LIMIT 1;', false, $id))
+ {
+ throw new UserException('Cet exercice ne peut être supprimé car des opérations comptables y sont liées.');
+ }
+
+ $db->simpleExec('DELETE FROM compta_exercices WHERE id = ?;', (int)$id);
+
+ return true;
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT *, strftime(\'%s\', debut) AS debut,
+ strftime(\'%s\', fin) AS fin FROM compta_exercices WHERE id = ?;', true, (int)$id);
+ }
+
+ public function getCurrent()
+ {
+ $db = DB::getInstance();
+ return $db->querySingle('SELECT *, strftime(\'%s\', debut) AS debut, strftime(\'%s\', fin) FROM compta_exercices
+ WHERE cloture = 0 LIMIT 1;', true);
+ }
+
+ public function getCurrentId()
+ {
+ $db = DB::getInstance();
+ return $db->querySingle('SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1;');
+ }
+
+ public function getList()
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetchAssocKey('SELECT id, *, strftime(\'%s\', debut) AS debut,
+ strftime(\'%s\', fin) AS fin,
+ (SELECT COUNT(*) FROM compta_journal WHERE id_exercice = compta_exercices.id) AS nb_operations
+ FROM compta_exercices ORDER BY fin DESC;', SQLITE3_ASSOC);
+ }
+
+ protected function _checkFields(&$data)
+ {
+ if (empty($data['libelle']) || !trim($data['libelle']))
+ {
+ throw new UserException('Le libellé ne peut rester vide.');
+ }
+
+ $data['libelle'] = trim($data['libelle']);
+
+ if (empty($data['debut']) || !checkdate(substr($data['debut'], 5, 2), substr($data['debut'], 8, 2), substr($data['debut'], 0, 4)))
+ {
+ throw new UserException('Date de début vide ou invalide.');
+ }
+
+ if (empty($data['fin']) || !checkdate(substr($data['fin'], 5, 2), substr($data['fin'], 8, 2), substr($data['fin'], 0, 4)))
+ {
+ throw new UserException('Date de fin vide ou invalide.');
+ }
+
+ return true;
+ }
+
+
+ public function getJournal($exercice)
+ {
+ $db = DB::getInstance();
+ $query = 'SELECT *, strftime(\'%s\', date) AS date FROM compta_journal
+ WHERE id_exercice = '.(int)$exercice.' ORDER BY date, id;';
+ return $db->simpleStatementFetch($query);
+ }
+
+ public function getGrandLivre($exercice)
+ {
+ $db = DB::getInstance();
+ $livre = ['classes' => [], 'debit' => 0.0, 'credit' => 0.0];
+
+ $res = $db->prepare('SELECT compte FROM
+ (SELECT compte_debit AS compte FROM compta_journal
+ WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_debit
+ UNION
+ SELECT compte_credit AS compte FROM compta_journal
+ WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_credit)
+ ORDER BY base64(compte) COLLATE BINARY ASC;'
+ )->execute();
+
+ while ($row = $res->fetchArray(SQLITE3_NUM))
+ {
+ $compte = $row[0];
+
+ if (is_null($compte))
+ continue;
+
+ $classe = substr($compte, 0, 1);
+ $parent = substr($compte, 0, 2);
+
+ if (!array_key_exists($classe, $livre['classes']))
+ {
+ $livre['classes'][$classe] = [];
+ }
+
+ if (!array_key_exists($parent, $livre['classes'][$classe]))
+ {
+ $livre['classes'][$classe][$parent] = [
+ 'total' => 0.0,
+ 'comptes' => [],
+ ];
+ }
+
+ $livre['classes'][$classe][$parent]['comptes'][$compte] = ['debit' => 0.0, 'credit' => 0.0, 'journal' => []];
+
+ $livre['classes'][$classe][$parent]['comptes'][$compte]['journal'] = $db->simpleStatementFetch(
+ 'SELECT *, strftime(\'%s\', date) AS date FROM (
+ SELECT * FROM compta_journal WHERE compte_debit = :compte AND id_exercice = '.(int)$exercice.'
+ UNION
+ SELECT * FROM compta_journal WHERE compte_credit = :compte AND id_exercice = '.(int)$exercice.'
+ )
+ ORDER BY date, numero_piece, id;', SQLITE3_ASSOC, ['compte' => $compte]);
+
+ $debit = (float) $db->simpleQuerySingle(
+ 'SELECT SUM(montant) FROM compta_journal WHERE compte_debit = ? AND id_exercice = '.(int)$exercice.';',
+ false, $compte);
+
+ $credit = (float) $db->simpleQuerySingle(
+ 'SELECT SUM(montant) FROM compta_journal WHERE compte_credit = ? AND id_exercice = '.(int)$exercice.';',
+ false, $compte);
+
+ $livre['classes'][$classe][$parent]['comptes'][$compte]['debit'] = $debit;
+ $livre['classes'][$classe][$parent]['comptes'][$compte]['credit'] = $credit;
+
+ $livre['classes'][$classe][$parent]['total'] += $debit;
+ $livre['classes'][$classe][$parent]['total'] -= $credit;
+
+ $livre['debit'] += $debit;
+ $livre['credit'] += $credit;
+ }
+
+ $res->finalize();
+
+ return $livre;
+ }
+
+ public function getCompteResultat($exercice)
+ {
+ $db = DB::getInstance();
+
+ $charges = ['comptes' => [], 'total' => 0.0];
+ $produits = ['comptes' => [], 'total' => 0.0];
+ $resultat = 0.0;
+
+ $res = $db->prepare('SELECT compte, SUM(debit), SUM(credit)
+ FROM
+ (SELECT compte_debit AS compte, SUM(montant) AS debit, 0 AS credit
+ FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_debit
+ UNION
+ SELECT compte_credit AS compte, 0 AS debit, SUM(montant) AS credit
+ FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_credit)
+ WHERE compte LIKE \'6%\' OR compte LIKE \'7%\'
+ GROUP BY compte
+ ORDER BY base64(compte) COLLATE BINARY ASC;'
+ )->execute();
+
+ while ($row = $res->fetchArray(SQLITE3_NUM))
+ {
+ list($compte, $debit, $credit) = $row;
+ $classe = substr($compte, 0, 1);
+ $parent = substr($compte, 0, 2);
+
+ if ($classe == 6)
+ {
+ if (!isset($charges['comptes'][$parent]))
+ {
+ $charges['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0];
+ }
+
+ $solde = round($debit - $credit, 2);
+
+ if (empty($solde))
+ continue;
+
+ $charges['comptes'][$parent]['comptes'][$compte] = $solde;
+ $charges['total'] += $solde;
+ $charges['comptes'][$parent]['solde'] += $solde;
+ }
+ elseif ($classe == 7)
+ {
+ if (!isset($produits['comptes'][$parent]))
+ {
+ $produits['comptes'][$parent] = ['comptes' => [], 'solde' => 0.0];
+ }
+
+ $solde = round($credit - $debit, 2);
+
+ if (empty($solde))
+ continue;
+
+ $produits['comptes'][$parent]['comptes'][$compte] = $solde;
+ $produits['total'] += $solde;
+ $produits['comptes'][$parent]['solde'] += $solde;
+ }
+ }
+
+ $res->finalize();
+
+ $resultat = $produits['total'] - $charges['total'];
+
+ return ['charges' => $charges, 'produits' => $produits, 'resultat' => $resultat];
+ }
+
+ /**
+ * Calculer le bilan comptable pour l'exercice $exercice
+ * @param integer $exercice ID de l'exercice dont il faut produire le bilan
+ * @param boolean $resultat true s'il faut calculer le résultat de l'exercice (utile pour un exercice en cours)
+ * @return array Un tableau multi-dimensionnel avec deux clés : actif et passif
+ */
+ public function getBilan($exercice)
+ {
+ $db = DB::getInstance();
+
+ $include = [Compta_Comptes::ACTIF, Compta_Comptes::PASSIF,
+ Compta_Comptes::PASSIF | Compta_Comptes::ACTIF];
+
+ $actif = ['comptes' => [], 'total' => 0.0];
+ $passif = ['comptes' => [], 'total' => 0.0];
+
+ $resultat = $this->getCompteResultat($exercice);
+
+ if ($resultat['resultat'] >= 0)
+ {
+ $passif['comptes']['12'] = [
+ 'comptes' => ['120' => $resultat['resultat']],
+ 'solde' => $resultat['resultat']
+ ];
+
+ $passif['total'] = $resultat['resultat'];
+ }
+ else
+ {
+ $passif['comptes']['12'] = [
+ 'comptes' => ['129' => $resultat['resultat']],
+ 'solde' => $resultat['resultat']
+ ];
+
+ $passif['total'] = $resultat['resultat'];
+ }
+
+ // Y'a sûrement moyen d'améliorer tout ça pour que le maximum de travail
+ // soit fait au niveau du SQL, mais pour le moment ça marche
+ $res = $db->prepare('SELECT compte, debit, credit, (SELECT position FROM compta_comptes WHERE id = compte) AS position
+ FROM
+ (SELECT compte_debit AS compte, SUM(montant) AS debit, NULL AS credit
+ FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_debit
+ UNION
+ SELECT compte_credit AS compte, NULL AS debit, SUM(montant) AS credit
+ FROM compta_journal WHERE id_exercice = '.(int)$exercice.' GROUP BY compte_credit)
+ WHERE compte IN (SELECT id FROM compta_comptes WHERE position IN ('.implode(', ', $include).'))
+ ORDER BY base64(compte) COLLATE BINARY ASC;'
+ )->execute();
+
+ while ($row = $res->fetchArray(SQLITE3_NUM))
+ {
+ list($compte, $debit, $credit, $position) = $row;
+ $parent = substr($compte, 0, 2);
+ $classe = $compte[0];
+
+ if (($position & Compta_Comptes::ACTIF) && ($position & Compta_Comptes::PASSIF))
+ {
+ $solde = $debit - $credit;
+
+ if ($solde > 0)
+ $position = 'actif';
+ elseif ($solde < 0)
+ $position = 'passif';
+ else
+ continue;
+
+ $solde = abs($solde);
+ }
+ else if ($position & Compta_Comptes::ACTIF)
+ {
+ $position = 'actif';
+ $solde = $debit - $credit;
+ }
+ else if ($position & Compta_Comptes::PASSIF)
+ {
+ $position = 'passif';
+ $solde = $credit - $debit;
+ }
+ else
+ {
+ continue;
+ }
+
+ if (!isset(${$position}['comptes'][$parent]))
+ {
+ ${$position}['comptes'][$parent] = ['comptes' => [], 'solde' => 0];
+ }
+
+ if (!isset(${$position}['comptes'][$parent]['comptes'][$compte]))
+ {
+ ${$position}['comptes'][$parent]['comptes'][$compte] = 0;
+ }
+
+ $solde = round($solde, 2);
+ ${$position}['comptes'][$parent]['comptes'][$compte] += $solde;
+ ${$position}['total'] += $solde;
+ ${$position}['comptes'][$parent]['solde'] += $solde;
+ }
+
+ $res->finalize();
+
+ // Suppression des soldes nuls
+ foreach ($passif['comptes'] as $parent=>$p)
+ {
+ if ($p['solde'] == 0)
+ {
+ unset($passif['comptes'][$parent]);
+ continue;
+ }
+
+ foreach ($p['comptes'] as $id=>$solde)
+ {
+ if ($solde == 0)
+ {
+ unset($passif['comptes'][$parent]['comptes'][$id]);
+ }
+ }
+ }
+
+ foreach ($actif['comptes'] as $parent=>$p)
+ {
+ if (empty($p['solde']))
+ {
+ unset($actif['comptes'][$parent]);
+ continue;
+ }
+
+ foreach ($p['comptes'] as $id=>$solde)
+ {
+ if (empty($solde))
+ {
+ unset($actif['comptes'][$parent]['comptes'][$id]);
+ }
+ }
+ }
+
+ return ['actif' => $actif, 'passif' => $passif];
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.compta_import.php b/include/class.compta_import.php
new file mode 100644
index 0000000..73fd99b
--- /dev/null
+++ b/include/class.compta_import.php
@@ -0,0 +1,387 @@
+prepare('SELECT
+ journal.id,
+ strftime(\'%d/%m/%Y\', date) AS date,
+ (CASE cat.type WHEN 1 THEN \'Recette\' WHEN -1 THEN \'Dépense\' ELSE \'Autre\' END) AS type,
+ (CASE cat.intitule WHEN NULL THEN \'\' ELSE cat.intitule END) AS cat,
+ journal.libelle,
+ montant,
+ compte_debit,
+ debit.libelle AS libelle_debit,
+ compte_credit,
+ credit.libelle AS libelle_credit,
+ (CASE moyen_paiement WHEN NULL THEN \'\' ELSE moyen.nom END) AS moyen,
+ numero_cheque,
+ numero_piece,
+ remarques
+ FROM compta_journal AS journal
+ LEFT JOIN compta_categories AS cat ON cat.id = journal.id_categorie
+ LEFT JOIN compta_comptes AS debit ON debit.id = journal.compte_debit
+ LEFT JOIN compta_comptes AS credit ON credit.id = journal.compte_credit
+ LEFT JOIN compta_moyens_paiement AS moyen ON moyen.code = journal.moyen_paiement
+ WHERE id_exercice = '.(int)$exercice.'
+ ORDER BY journal.date;
+ ')->execute();
+
+ $fp = fopen('php://output', 'w');
+
+ fputcsv($fp, $this->csv_header);
+
+ while ($row = $res->fetchArray(SQLITE3_ASSOC))
+ {
+ fputcsv($fp, $row);
+ }
+
+ fclose($fp);
+
+ return true;
+ }
+
+ public function fromCSV($path)
+ {
+ if (!file_exists($path) || !is_readable($path))
+ {
+ throw new \RuntimeException('Fichier inconnu : '.$path);
+ }
+
+ $fp = fopen($path, 'r');
+
+ if (!$fp)
+ {
+ return false;
+ }
+
+ $db = DB::getInstance();
+ $db->exec('BEGIN;');
+ $comptes = new Compta_Comptes;
+ $banques = new Compta_Comptes_Bancaires;
+ $cats = new Compta_Categories;
+ $journal = new Compta_Journal;
+
+ $columns = array_flip($this->csv_header);
+ $liste_comptes = $db->simpleStatementFetchAssoc('SELECT id, id FROM compta_comptes;');
+ $liste_cats = $db->simpleStatementFetchAssoc('SELECT intitule, id FROM compta_categories;');
+ $liste_moyens = $cats->listMoyensPaiement();
+
+ $col = function($column) use (&$row, &$columns)
+ {
+ if (!isset($columns[$column]))
+ return null;
+
+ if (!isset($row[$columns[$column]]))
+ return null;
+
+ return $row[$columns[$column]];
+ };
+
+ $line = 0;
+ $delim = utils::find_csv_delim($fp);
+
+ while (!feof($fp))
+ {
+ $row = fgetcsv($fp, 4096, $delim);
+ $line++;
+
+ if (empty($row))
+ {
+ continue;
+ }
+
+ if ($line === 1)
+ {
+ if (trim($row[0]) != 'Numéro mouvement')
+ {
+ throw new UserException('Erreur sur la ligne ' . $line . ' : l\'entête des colonnes est absent ou incorrect.');
+ }
+
+ continue;
+ }
+
+ if (count($row) != count($columns))
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
+ }
+
+ if (trim($row[0]) !== '' && !is_numeric($row[0]))
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : la première colonne doit être vide ou contenir le numéro unique d\'opération.');
+ }
+
+ $id = $col('Numéro mouvement');
+ $date = $col('Date');
+
+ if (!preg_match('!^\d{2}/\d{2}/\d{4}$!', $date))
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : la date n\'est pas au format jj/mm/aaaa.');
+ }
+
+ $date = explode('/', $date);
+ $date = $date[2] . '-' . $date[1] . '-' . $date[0];
+
+ // En dehors de l'exercice courant
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices
+ WHERE (? < debut OR ? > fin) AND cloture = 0;', false, $date, $date))
+ {
+ continue;
+ }
+
+ $debit = $col('Compte de débit - numéro');
+ $credit = $col('Compte de crédit - numéro');
+
+ if (trim($debit) == '' && trim($credit) != '')
+ {
+ $debit = null;
+ }
+ elseif (trim($debit) != '' && trim($credit) == '')
+ {
+ $credit = null;
+ }
+
+ $cat = $col('Catégorie');
+ $moyen = strtoupper(substr($col('Moyen de paiement'), 0, 2));
+
+ if (!$moyen || !array_key_exists($moyen, $liste_moyens))
+ {
+ $moyen = false;
+ $cat = false;
+ }
+
+ if ($cat && !array_key_exists($cat, $liste_cats))
+ {
+ $cat = $moyen = false;
+ }
+
+ $data = [
+ 'libelle' => $col('Libellé'),
+ 'montant' => (float) $col('Montant'),
+ 'date' => $date,
+ 'compte_credit' => $credit,
+ 'compte_debit' => $debit,
+ 'numero_piece' => $col('Numéro de pièce'),
+ 'remarques' => $col('Remarques'),
+ ];
+
+ if ($cat)
+ {
+ $data['moyen_paiement'] = $moyen;
+ $data['numero_cheque'] = $col('Numéro de chèque');
+ $data['id_categorie'] = $liste_cats[$cat];
+ }
+
+ if (empty($id))
+ {
+ $journal->add($data);
+ }
+ else
+ {
+ $journal->edit($id, $data);
+ }
+ }
+
+ $db->exec('END;');
+
+ fclose($fp);
+ return true;
+ }
+
+ public function fromCitizen($path)
+ {
+ if (!file_exists($path) || !is_readable($path))
+ {
+ throw new \RuntimeException('Fichier inconnu : '.$path);
+ }
+
+ $fp = fopen($path, 'r');
+
+ if (!$fp)
+ {
+ return false;
+ }
+
+ $db = DB::getInstance();
+ $db->exec('BEGIN;');
+ $comptes = new Compta_Comptes;
+ $banques = new Compta_Comptes_Bancaires;
+ $cats = new Compta_Categories;
+ $journal = new Compta_Journal;
+
+ $columns = [];
+ $liste_comptes = $db->simpleStatementFetchAssoc('SELECT id, id FROM compta_comptes;');
+ $liste_cats = $db->simpleStatementFetchAssoc('SELECT intitule, id FROM compta_categories;');
+ $liste_moyens = $cats->listMoyensPaiement();
+
+ $get_compte = function ($compte, $intitule) use (&$liste_comptes, &$comptes, &$banques)
+ {
+ if (substr($compte, 0, 2) == '51')
+ {
+ $compte = '512' . substr($compte, -1);
+ }
+
+ // Création comptes
+ if (!array_key_exists($compte, $liste_comptes))
+ {
+ if (substr($compte, 0, 3) == '512')
+ {
+ $liste_comptes[$compte] = $banques->add([
+ 'libelle' => $intitule,
+ 'banque' => 'Inconnue',
+ ]);
+ }
+ else
+ {
+ $liste_comptes[$compte] = $comptes->add([
+ 'id' => $compte,
+ 'libelle' => $intitule,
+ 'parent' => substr($compte, 0, -1)
+ ]);
+ }
+ }
+
+ return $compte;
+ };
+
+ $col = function($column) use (&$row, &$columns)
+ {
+ if (!isset($columns[$column]))
+ return null;
+
+ if (!isset($row[$columns[$column]]))
+ return null;
+
+ return $row[$columns[$column]];
+ };
+
+ $line = 0;
+ $delim = utils::find_csv_delim($fp);
+
+ while (!feof($fp))
+ {
+ $row = fgetcsv($fp, 4096, $delim);
+ $line++;
+
+ if (empty($row))
+ {
+ continue;
+ }
+
+ if (empty($columns))
+ {
+ $columns = $row;
+ $columns = array_flip($columns);
+ continue;
+ }
+
+ $date = $col('Date');
+
+ if (!preg_match('!^\d{2}/\d{2}/\d{4}$!', $date))
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : la date n\'est pas au format jj/mm/aaaa.');
+ }
+
+ $date = explode('/', $date);
+ $date = $date[2] . '-' . $date[1] . '-' . $date[0];
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM compta_exercices
+ WHERE (? < debut OR ? > fin) AND cloture = 0;', false, $date, $date))
+ {
+ continue;
+ }
+
+ $debit = $get_compte($col('Compte débité - Numéro'), $col('Compte débité - Intitulé'));
+ $credit = $get_compte($col('Compte crédité - Numéro'), $col('Compte crédité - Intitulé'));
+
+ $cat = $col('Rubrique');
+ $moyen = strtoupper(substr($col('Moyen de paiement'), 0, 2));
+
+ if (!$moyen || !array_key_exists($moyen, $liste_moyens))
+ {
+ $moyen = false;
+ $cat = false;
+ }
+
+ if ($cat && !array_key_exists($cat, $liste_cats))
+ {
+ if ($col('Nature') == 'Recette')
+ {
+ $type = $cats::RECETTES;
+ $compte = $credit;
+ }
+ elseif ($col('Nature') == 'Dépense')
+ {
+ $type = $cats::DEPENSES;
+ $compte = $debit;
+ }
+ else
+ {
+ $type = $cats::AUTRES;
+ $cat = false;
+ }
+
+ if ($type != $cats::AUTRES)
+ {
+ $liste_cats[$cat] = $cats->add([
+ 'intitule' => $cat,
+ 'type' => $type,
+ 'compte' => $compte
+ ]);
+ }
+ }
+
+ $data = [
+ 'libelle' => $col('Libellé'),
+ 'montant' => $col('Montant'),
+ 'date' => $date,
+ 'compte_credit' => $credit,
+ 'compte_debit' => $debit,
+ 'numero_piece' => $col('Numéro de pièce'),
+ 'remarques' => $col('Remarques'),
+ ];
+
+ if ($cat)
+ {
+ $data['moyen_paiement'] = $moyen;
+ $data['numero_cheque'] = $col('Numéro de chèque');
+ $data['id_categorie'] = $liste_cats[$cat];
+ }
+
+ $journal->add($data);
+ }
+
+ $db->exec('END;');
+
+ fclose($fp);
+ return true;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.compta_journal.php b/include/class.compta_journal.php
new file mode 100644
index 0000000..9ff8c95
--- /dev/null
+++ b/include/class.compta_journal.php
@@ -0,0 +1,363 @@
+querySingle('SELECT id FROM compta_exercices WHERE cloture = 0 LIMIT 1;');
+
+ if (!$id)
+ {
+ throw new UserException('Aucun exercice en cours.');
+ }
+
+ return $id;
+ }
+
+ public function checkExercice()
+ {
+ return $this->_getCurrentExercice();
+ }
+
+ protected function _checkOpenExercice($id)
+ {
+ if (is_null($id))
+ return true;
+
+ $db = DB::getInstance();
+ $id = $db->simpleQuerySingle('SELECT id FROM compta_exercices
+ WHERE cloture = 0 AND id = ? LIMIT 1;', false, (int)$id);
+
+ if ($id)
+ return true;
+
+ return false;
+ }
+
+ public function getSolde($id_compte, $inclure_sous_comptes = false)
+ {
+ $db = DB::getInstance();
+ $exercice = $this->_getCurrentExercice();
+ $compte = $inclure_sous_comptes
+ ? 'LIKE \'' . $db->escapeString(trim($id_compte)) . '%\''
+ : '= \'' . $db->escapeString(trim($id_compte)) . '\'';
+
+ $debit = 'COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_debit '.$compte.' AND id_exercice = '.(int)$exercice.'), 0)';
+ $credit = 'COALESCE((SELECT SUM(montant) FROM compta_journal WHERE compte_credit '.$compte.' AND id_exercice = '.(int)$exercice.'), 0)';
+
+ // L'actif augmente au débit, le passif au crédit
+ $position = $db->simpleQuerySingle('SELECT position FROM compta_comptes WHERE id = ?;', false, $id_compte);
+
+ if (($position & Compta_Comptes::ACTIF) || ($position & Compta_Comptes::CHARGE))
+ {
+ $query = $debit . ' - ' . $credit;
+ }
+ else
+ {
+ $query = $credit . ' - ' . $debit;
+ }
+
+ return $db->querySingle('SELECT ' . $query . ';');
+ }
+
+ public function getJournalCompte($compte, $inclure_sous_comptes = false)
+ {
+ $db = DB::getInstance();
+
+ $position = $db->simpleQuerySingle('SELECT position FROM compta_comptes WHERE id = ?;', false, $compte);
+
+ $exercice = $this->_getCurrentExercice();
+ $compte = $inclure_sous_comptes
+ ? 'LIKE \'' . $db->escapeString(trim($compte)) . '%\''
+ : '= \'' . $db->escapeString(trim($compte)) . '\'';
+
+ // L'actif et les charges augmentent au débit, le passif et les produits au crédit
+ if (($position & Compta_Comptes::ACTIF) || ($position & Compta_Comptes::CHARGE))
+ {
+ $d = '';
+ $c = '-';
+ }
+ else
+ {
+ $d = '-';
+ $c = '';
+ }
+
+ $query = 'SELECT *, strftime(\'%s\', date) AS date,
+ running_sum(CASE WHEN compte_debit '.$compte.' THEN '.$d.'montant ELSE '.$c.'montant END) AS solde
+ FROM compta_journal WHERE (compte_debit '.$compte.' OR compte_credit '.$compte.') AND id_exercice = '.(int)$exercice.'
+ ORDER BY date ASC;';
+
+ // Obligatoire pour bien taper dans l'index de la date
+ // sinon running_sum est appelé 2 fois et ça marche pas du coup
+ // FIXME mettre ça ailleurs pour que ça soit appelé moins souvent
+ $db->exec('ANALYZE compta_journal;');
+
+ $db->resetRunningSum();
+ return $db->simpleStatementFetch($query);
+ }
+
+ public function add($data)
+ {
+ $this->_checkFields($data);
+
+ $db = DB::getInstance();
+
+ $data['id_exercice'] = $this->_getCurrentExercice();
+
+ $db->simpleInsert('compta_journal', $data);
+ $id = $db->lastInsertRowId();
+
+ return $id;
+ }
+
+ public function edit($id, $data)
+ {
+ $db = DB::getInstance();
+
+ // Vérification que l'on peut éditer cette opération
+ if (!$this->_checkOpenExercice($db->simpleQuerySingle('SELECT id_exercice FROM compta_journal WHERE id = ?;', false, $id)))
+ {
+ throw new UserException('Cette opération fait partie d\'un exercice qui a été clôturé.');
+ }
+
+ $this->_checkFields($data);
+
+ $db->simpleUpdate('compta_journal', $data,
+ 'id = \''.trim($id).'\'');
+
+ return true;
+ }
+
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+
+ // Vérification que l'on peut éditer cette opération
+ if (!$this->_checkOpenExercice($db->simpleQuerySingle('SELECT id_exercice FROM compta_journal WHERE id = ?;', false, $id)))
+ {
+ throw new UserException('Cette opération fait partie d\'un exercice qui a été clôturé.');
+ }
+
+ $db->simpleExec('DELETE FROM membres_operations WHERE id_operation = ?;', (int)$id);
+ $db->simpleExec('DELETE FROM compta_journal WHERE id = ?;', (int)$id);
+
+ return true;
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT *, strftime(\'%s\', date) AS date FROM compta_journal WHERE id = ?;', true, $id);
+ }
+
+ public function countForMember($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT COUNT(*)
+ FROM compta_journal WHERE id_auteur = ?;', false, (int)$id);
+ }
+
+ public function listForMember($id, $exercice)
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT * FROM compta_journal
+ WHERE id_auteur = ? AND id_exercice = ?;', \SQLITE3_ASSOC, (int)$id, (int)$exercice);
+ }
+
+ protected function _checkFields(&$data)
+ {
+ $db = DB::getInstance();
+
+ if (empty($data['libelle']) || !trim($data['libelle']))
+ {
+ throw new UserException('Le libellé ne peut rester vide.');
+ }
+
+ $data['libelle'] = trim($data['libelle']);
+
+ if (!empty($data['moyen_paiement'])
+ && !$db->simpleQuerySingle('SELECT 1 FROM compta_moyens_paiement WHERE code = ?;', false, $data['moyen_paiement']))
+ {
+ throw new UserException('Moyen de paiement invalide.');
+ }
+
+ if (empty($data['date']) || !utils::checkDate($data['date']))
+ {
+ throw new UserException('Date vide ou invalide.');
+ }
+
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_exercices WHERE cloture = 0
+ AND debut <= :date AND fin >= :date;', false, ['date' => $data['date']]))
+ {
+ throw new UserException('La date ne correspond pas à l\'exercice en cours.');
+ }
+
+ if (empty($data['moyen_paiement']))
+ {
+ $data['moyen_paiement'] = null;
+ $data['numero_cheque'] = null;
+ }
+ else
+ {
+ $data['moyen_paiement'] = strtoupper($data['moyen_paiement']);
+
+ if ($data['moyen_paiement'] != 'CH')
+ {
+ $data['numero_cheque'] = null;
+ }
+
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_moyens_paiement WHERE code = ? LIMIT 1;',
+ false, $data['moyen_paiement']))
+ {
+ throw new UserException('Moyen de paiement invalide.');
+ }
+ }
+
+ $data['montant'] = str_replace(',', '.', $data['montant']);
+ $data['montant'] = (float)$data['montant'];
+
+ if ($data['montant'] <= 0)
+ {
+ throw new UserException('Le montant ne peut être égal ou inférieur à zéro.');
+ }
+
+ foreach (['remarques', 'numero_piece', 'numero_cheque'] as $champ)
+ {
+ if (empty($data[$champ]) || !trim($data[$champ]))
+ {
+ $data[$champ] = '';
+ }
+ else
+ {
+ $data[$champ] = trim($data[$champ]);
+ }
+ }
+
+ if (!array_key_exists('compte_debit', $data) ||
+ (!is_null($data['compte_debit']) &&
+ !$db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['compte_debit'])))
+ {
+ throw new UserException('Compte débité inconnu.');
+ }
+
+ if (!array_key_exists('compte_credit', $data) ||
+ (!is_null($data['compte_credit']) &&
+ !$db->simpleQuerySingle('SELECT 1 FROM compta_comptes WHERE id = ?;', false, $data['compte_credit'])))
+ {
+ throw new UserException('Compte crédité inconnu.');
+ }
+
+ $data['compte_credit'] = is_null($data['compte_credit']) ? null : strtoupper(trim($data['compte_credit']));
+ $data['compte_debit'] = is_null($data['compte_debit']) ? null : strtoupper(trim($data['compte_debit']));
+
+ if ($data['compte_credit'] == $data['compte_debit'])
+ {
+ throw new UserException('Compte crédité identique au compte débité.');
+ }
+
+ if (isset($data['id_categorie']))
+ {
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE id = ?;', false, (int)$data['id_categorie']))
+ {
+ throw new UserException('Catégorie inconnue.');
+ }
+
+ $data['id_categorie'] = (int)$data['id_categorie'];
+ }
+ else
+ {
+ $data['id_categorie'] = NULL;
+ }
+
+ if (isset($data['id_auteur']))
+ {
+ $data['id_auteur'] = (int)$data['id_auteur'];
+ }
+
+ return true;
+ }
+
+ public function getListForCategory($type = null, $cat = null)
+ {
+ $db = DB::getInstance();
+ $exercice = $this->_getCurrentExercice();
+
+ $query = 'SELECT compta_journal.*, strftime(\'%s\', compta_journal.date) AS date ';
+
+ if (is_null($cat) && !is_null($type))
+ {
+ $query.= ', compta_categories.intitule AS categorie
+ FROM compta_journal LEFT JOIN compta_categories
+ ON compta_journal.id_categorie = compta_categories.id ';
+ }
+ else
+ {
+ $query.= ' FROM compta_journal ';
+ }
+
+ $query .= ' WHERE ';
+
+ if (!is_null($cat))
+ {
+ $query .= 'id_categorie = ' . (int)$cat;
+ }
+ elseif (is_null($type) && is_null($cat))
+ {
+ $query .= 'id_categorie IS NULL';
+ }
+ else
+ {
+ $query.= 'id_categorie IN (SELECT id FROM compta_categories WHERE type = '.(int)$type.')';
+ }
+
+ $query .= ' AND id_exercice = ' . (int)$exercice;
+ $query .= ' ORDER BY date;';
+
+ return $db->simpleStatementFetch($query);
+ }
+
+ public function searchSQL($query)
+ {
+ $db = DB::getInstance();
+
+ if (!preg_match('/LIMIT\s+/', $query))
+ {
+ $query = preg_replace('/;?\s*$/', '', $query);
+ $query .= ' LIMIT 100';
+ }
+
+ $st = $db->prepare($query);
+
+ if (!$st->readOnly())
+ {
+ throw new UserException('Seules les requêtes en lecture sont autorisées.');
+ }
+
+ $res = $st->execute();
+ $out = [];
+
+ while ($row = $res->fetchArray(SQLITE3_ASSOC))
+ {
+ $out[] = $row;
+ }
+
+ return $out;
+ }
+
+ public function schemaSQL()
+ {
+ $db = DB::getInstance();
+
+ $tables = [
+ 'journal' => $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'compta_journal\';'),
+ ];
+
+ return $tables;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.compta_stats.php b/include/class.compta_stats.php
new file mode 100644
index 0000000..335fbe0
--- /dev/null
+++ b/include/class.compta_stats.php
@@ -0,0 +1,122 @@
+simpleStatementFetch('SELECT COUNT(*) AS nb, id_categorie
+ FROM compta_journal
+ WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = ?)
+ AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0)
+ GROUP BY id_categorie ORDER BY nb DESC;', SQLITE3_ASSOC, $type);
+ }
+
+ public function repartitionRecettes()
+ {
+ return $this->_parRepartitionCategorie(Compta_Categories::RECETTES);
+ }
+
+ public function repartitionDepenses()
+ {
+ return $this->_parRepartitionCategorie(Compta_Categories::DEPENSES);
+ }
+
+ protected function _parType($type)
+ {
+ return $this->getStats('SELECT strftime(\'%Y%m\', date) AS date,
+ SUM(montant) FROM compta_journal
+ WHERE id_categorie IN (SELECT id FROM compta_categories WHERE type = '.$type.')
+ AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0)
+ GROUP BY strftime(\'%Y-%m\', date) ORDER BY date;');
+ }
+
+ public function recettes()
+ {
+ return $this->_parType(Compta_Categories::RECETTES);
+ }
+
+ public function depenses()
+ {
+ return $this->_parType(Compta_Categories::DEPENSES);
+ }
+
+ public function soldeCompte($compte, $augmente = 'debit', $diminue = 'credit')
+ {
+ $db = DB::getInstance();
+
+ if (strpos($compte, '%') !== false)
+ {
+ $compte = 'LIKE \''. $db->escapeString($compte) . '\'';
+ }
+ else
+ {
+ $compte = '= \''. $db->escapeString($compte) . '\'';
+ }
+
+ $stats = $this->getStats('SELECT strftime(\'%Y%m\', date) AS date,
+ (COALESCE((SELECT SUM(montant) FROM compta_journal
+ WHERE compte_'.$augmente.' '.$compte.' AND id_exercice = cj.id_exercice
+ AND date >= strftime(\'%Y-%m-01\', cj.date)
+ AND date <= strftime(\'%Y-%m-31\', cj.date)), 0)
+ - COALESCE((SELECT SUM(montant) FROM compta_journal
+ WHERE compte_'.$diminue.' '.$compte.' AND id_exercice = cj.id_exercice
+ AND date >= strftime(\'%Y-%m-01\', cj.date)
+ AND date <= strftime(\'%Y-%m-31\', cj.date)), 0)
+ ) AS solde
+ FROM compta_journal AS cj
+ WHERE (compte_debit '.$compte.' OR compte_credit '.$compte.')
+ AND id_exercice = (SELECT id FROM compta_exercices WHERE cloture = 0)
+ GROUP BY strftime(\'%Y-%m\', date) ORDER BY date;');
+
+ $c = 0;
+ foreach ($stats as $k=>$v)
+ {
+ $c += $v;
+ $stats[$k] = $c;
+ }
+
+ return $stats;
+ }
+
+ public function getStats($query)
+ {
+ $db = DB::getInstance();
+
+ $data = $db->simpleStatementFetchAssoc($query);
+
+ $e = $db->querySingle('SELECT *, strftime(\'%s\', debut) AS debut,
+ strftime(\'%s\', fin) AS fin FROM compta_exercices WHERE cloture = 0;', true);
+
+ $y = date('Y', $e['debut']);
+ $m = date('m', $e['debut']);
+ $max = date('Ym', $e['fin']);
+
+ while ($y . $m <= $max)
+ {
+ if (!isset($data[$y . $m]))
+ {
+ $data[$y . $m] = 0;
+ }
+
+ if ($m == 12)
+ {
+ $m = '01';
+ $y++;
+ }
+ else
+ {
+ $m++;
+ $m = str_pad((int)$m, 2, '0', STR_PAD_LEFT);
+ }
+ }
+
+ ksort($data);
+
+ return $data;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.config.php b/include/class.config.php
new file mode 100644
index 0000000..6ee1bd3
--- /dev/null
+++ b/include/class.config.php
@@ -0,0 +1,326 @@
+fields_types = [
+ 'nom_asso' => $string,
+ 'adresse_asso' => $string,
+ 'email_asso' => $string,
+ 'site_asso' => $string,
+
+ 'monnaie' => $string,
+ 'pays' => $string,
+
+ 'champs_membres' => $object,
+
+ 'email_envoi_automatique'=> $string,
+
+ 'categorie_membres' => $int,
+
+ 'categorie_dons' => $int,
+ 'categorie_cotisations' => $int,
+
+ 'accueil_wiki' => $string,
+ 'accueil_connexion' => $string,
+
+ 'frequence_sauvegardes' => $int,
+ 'nombre_sauvegardes' => $int,
+
+ 'champ_identifiant' => $string,
+ 'champ_identite' => $string,
+
+ 'version' => $string,
+ ];
+
+ $db = DB::getInstance();
+
+ $this->config = $db->simpleStatementFetchAssoc('SELECT cle, valeur FROM config ORDER BY cle;');
+
+ foreach ($this->config as $key=>&$value)
+ {
+ if (!array_key_exists($key, $this->fields_types))
+ {
+ // Ancienne clé de config qui n'est plus utilisée
+ continue;
+ }
+
+ if (is_array($this->fields_types[$key]))
+ {
+ $value = explode(',', $value);
+ }
+ elseif ($key == 'champs_membres')
+ {
+ $value = new Champs_Membres((string)$value);
+ }
+ else
+ {
+ settype($value, gettype($this->fields_types[$key]));
+ }
+ }
+ }
+
+ public function __destruct()
+ {
+ if (!empty($this->modified))
+ {
+ //echo 'Il y a des champs modifiés non sauvés dans '.__CLASS__.' !
';
+ }
+ }
+
+ public function save()
+ {
+ if (empty($this->modified))
+ return true;
+
+ $values = [];
+
+ $db = DB::getInstance();
+ $db->exec('BEGIN;');
+
+ foreach ($this->modified as $key=>$modified)
+ {
+ $value = $this->config[$key];
+
+ if (is_array($value))
+ {
+ $value = implode(',', $value);
+ }
+ elseif (is_object($value))
+ {
+ $value = (string) $value;
+ }
+
+ $db->simpleExec('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);',
+ $key, $value);
+ }
+
+ if (!empty($this->modified['champ_identifiant']))
+ {
+ // Mettre les champs identifiant vides à NULL pour pouvoir créer un index unique
+ $db->exec('UPDATE membres SET '.$this->get('champ_identifiant').' = NULL
+ WHERE '.$this->get('champ_identifiant').' = "";');
+
+ // Création de l'index unique
+ $db->exec('DROP INDEX IF EXISTS membres_identifiant;');
+ $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$this->get('champ_identifiant').');');
+ }
+
+ $db->exec('END;');
+
+ $this->modified = [];
+
+ return true;
+ }
+
+ public function get($key)
+ {
+ if (!array_key_exists($key, $this->fields_types))
+ {
+ throw new \OutOfBoundsException('Ce champ est inconnu.');
+ }
+
+ if (!array_key_exists($key, $this->config))
+ {
+ return null;
+ }
+
+ return $this->config[$key];
+ }
+
+ public function getVersion()
+ {
+ if (!array_key_exists('version', $this->config))
+ {
+ return '0';
+ }
+
+ return $this->config['version'];
+ }
+
+ public function setVersion($version)
+ {
+ $this->config['version'] = $version;
+
+ $db = DB::getInstance();
+ $db->simpleExec('INSERT OR REPLACE INTO config (cle, valeur) VALUES (?, ?);',
+ 'version', $version);
+
+ return true;
+ }
+
+ public function set($key, $value)
+ {
+ if (!array_key_exists($key, $this->fields_types))
+ {
+ throw new \OutOfBoundsException('Ce champ est inconnu.');
+ }
+
+ if (is_array($this->fields_types[$key]))
+ {
+ $value = !empty($value) ? (array) $value : [];
+ }
+ elseif (is_int($this->fields_types[$key]))
+ {
+ $value = (int) $value;
+ }
+ elseif (is_float($this->fields_types[$key]))
+ {
+ $value = (float) $value;
+ }
+ elseif (is_bool($this->fields_types[$key]))
+ {
+ $value = (bool) $value;
+ }
+ elseif (is_string($this->fields_types[$key]))
+ {
+ $value = (string) $value;
+ }
+
+ switch ($key)
+ {
+ case 'nom_asso':
+ {
+ if (!trim($value))
+ {
+ throw new UserException('Le nom de l\'association ne peut rester vide.');
+ }
+ break;
+ }
+ case 'accueil_wiki':
+ case 'accueil_connexion':
+ {
+ if (!trim($value))
+ {
+ $key = str_replace('accueil_', '', $key);
+ throw new UserException('Le nom de la page d\'accueil ' . $key . ' ne peut rester vide.');
+ }
+ break;
+ }
+ case 'email_asso':
+ case 'email_envoi_automatique':
+ {
+ if (!filter_var($value, FILTER_VALIDATE_EMAIL))
+ {
+ throw new UserException('Adresse e-mail invalide.');
+ }
+ break;
+ }
+ case 'champs_membres':
+ {
+ if (!($value instanceOf Champs_Membres))
+ {
+ throw new \UnexpectedValueException('$value doit être de type Champs_Membres');
+ }
+ break;
+ }
+ case 'champ_identite':
+ case 'champ_identifiant':
+ {
+ $champs = $this->get('champs_membres');
+ $db = DB::getInstance();
+
+ // Vérification que le champ existe bien
+ if (!$champs->get($value))
+ {
+ throw new UserException('Le champ '.$value.' n\'existe pas pour la configuration de '.$key);
+ }
+
+ // Vérification que le champ est unique pour l'identifiant
+ if ($key == 'champ_identifiant'
+ && !$db->simpleQuerySingle('SELECT (COUNT(DISTINCT '.$value.') = COUNT(*))
+ FROM membres WHERE '.$value.' IS NOT NULL AND '.$value.' != \'\';'))
+ {
+ throw new UserException('Le champ '.$value.' comporte des doublons et ne peut donc pas servir comme identifiant pour la connexion.');
+ }
+ break;
+ }
+ case 'categorie_cotisations':
+ case 'categorie_dons':
+ {
+ return false;
+ $db = DB::getInstance();
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_categories WHERE id = ?;', false, $value))
+ {
+ throw new UserException('Champ '.$key.' : La catégorie comptable numéro \''.$value.'\' ne semble pas exister.');
+ }
+ break;
+ }
+ case 'categorie_membres':
+ {
+ $db = DB::getInstance();
+ if (!$db->simpleQuerySingle('SELECT 1 FROM membres_categories WHERE id = ?;', false, $value))
+ {
+ throw new UserException('La catégorie de membres par défaut numéro \''.$value.'\' ne semble pas exister.');
+ }
+ break;
+ }
+ case 'monnaie':
+ {
+ if (!trim($value))
+ {
+ throw new UserException('La monnaie doit être renseignée.');
+ }
+
+ break;
+ }
+ case 'pays':
+ {
+ if (!trim($value) || !utils::getCountryName($value))
+ {
+ throw new UserException('Le pays renseigné est invalide.');
+ }
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (!isset($this->config[$key]) || $value !== $this->config[$key])
+ {
+ $this->config[$key] = $value;
+ $this->modified[$key] = true;
+ }
+
+ return true;
+ }
+
+ public function getFieldsTypes()
+ {
+ return $this->fields_types;
+ }
+
+ public function getConfig()
+ {
+ return $this->config;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.cotisations.php b/include/class.cotisations.php
new file mode 100644
index 0000000..9490784
--- /dev/null
+++ b/include/class.cotisations.php
@@ -0,0 +1,170 @@
+simpleQuerySingle('SELECT 1 FROM compta_categories WHERE id = ?;', false, (int) $data['id_categorie_compta']))
+ {
+ throw new UserException('Catégorie comptable inconnue');
+ }
+
+ $data['id_categorie_compta'] = (int) $data['id_categorie_compta'];
+ }
+ }
+
+ /**
+ * Ajouter une cotisation
+ * @param array $data Tableau des champs à insérer
+ * @return integer ID de la cotisation créée
+ */
+ public function add($data)
+ {
+ $db = DB::getInstance();
+
+ $this->_checkFields($data);
+
+ $db->simpleInsert('cotisations', $data);
+ $id = $db->lastInsertRowId();
+
+ return $id;
+ }
+
+ /**
+ * Modifier une cotisation
+ * @param integer $id ID de la cotisation à modifier
+ * @param array $data Tableau des champs à modifier
+ * @return bool true si succès
+ */
+ public function edit($id, $data)
+ {
+ $db = DB::getInstance();
+
+ $this->_checkFields($data);
+
+ return $db->simpleUpdate('cotisations', $data, 'id = \''.(int) $id.'\'');
+ }
+
+ /**
+ * Supprimer une cotisation
+ * @param integer $id ID de la cotisation à supprimer
+ * @return integer true en cas de succès
+ */
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+
+ $db->exec('BEGIN;');
+ $db->simpleExec('DELETE FROM cotisations_membres WHERE id_cotisation = ?;', (int) $id);
+ $db->simpleExec('DELETE FROM cotisations WHERE id = ?;', (int) $id);
+ $db->exec('END;');
+
+ return true;
+ }
+
+ /**
+ * Renvoie les infos sur une cotisation
+ * @param integer $id Numéro de la cotisation
+ * @return array Infos de la cotisation
+ */
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT co.*,
+ (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres WHERE id_cotisation = co.id) AS nb_membres,
+ (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres AS cm WHERE id_cotisation = co.id
+ AND ((co.duree IS NOT NULL AND date(cm.date, \'+\'||co.duree||\' days\') >= date())
+ OR (co.fin IS NOT NULL AND co.debut <= cm.date AND co.fin >= cm.date))) AS nb_a_jour
+ FROM cotisations AS co WHERE id = :id;', true, ['id' => (int) $id]);
+ }
+
+ public function listByName()
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT * FROM cotisations ORDER BY intitule;');
+ }
+
+ public function listCurrent()
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT * FROM cotisations WHERE fin >= date(\'now\') OR fin IS NULL
+ ORDER BY transliterate_to_ascii(intitule) COLLATE NOCASE;');
+ }
+
+ public function listCurrentWithStats()
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT co.*,
+ (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres WHERE id_cotisation = co.id) AS nb_membres,
+ (SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres AS cm WHERE id_cotisation = co.id
+ AND ((co.duree IS NOT NULL AND date(cm.date, \'+\'||co.duree||\' days\') >= date())
+ OR (co.fin IS NOT NULL AND co.debut <= cm.date AND co.fin >= cm.date))) AS nb_a_jour
+ FROM cotisations AS co WHERE fin >= date(\'now\') OR fin IS NULL
+ ORDER BY transliterate_to_ascii(intitule) COLLATE NOCASE;');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.cotisations_membres.php b/include/class.cotisations_membres.php
new file mode 100644
index 0000000..a895ee2
--- /dev/null
+++ b/include/class.cotisations_membres.php
@@ -0,0 +1,336 @@
+simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;', false, (int) $data['id_cotisation']))
+ {
+ throw new UserException('Cotisation inconnue.');
+ }
+
+ $data['id_cotisation'] = (int) $data['id_cotisation'];
+
+ if (empty($data['id_membre'])
+ || !$db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int) $data['id_membre']))
+ {
+ throw new UserException('Membre inconnu ou invalide.');
+ }
+
+ $data['id_membre'] = (int) $data['id_membre'];
+
+ if ($compta)
+ {
+ if (!isset($data['moyen_paiement']) || trim($data['moyen_paiement']) === '')
+ {
+ throw new UserException('Moyen de paiement inconnu ou invalide.');
+ }
+
+ if ($data['moyen_paiement'] != 'ES')
+ {
+ if (trim($data['banque']) == '')
+ {
+ throw new UserException('Le compte bancaire choisi est invalide.');
+ }
+
+ if (!$db->simpleQuerySingle('SELECT 1 FROM compta_comptes_bancaires WHERE id = ?;',
+ false, $data['banque']))
+ {
+ throw new UserException('Le compte bancaire choisi n\'existe pas.');
+ }
+ }
+
+ if (empty($data['montant']) || !is_numeric($data['montant']))
+ {
+ throw new UserException('Le montant indiqué n\'est pas un nombre valide.');
+ }
+ }
+ }
+
+ /**
+ * Enregistrer un événement de cotisation
+ * @param array $data Tableau des champs à insérer
+ * @return integer ID de l'événement créé
+ */
+ public function add($data)
+ {
+ $db = DB::getInstance();
+
+ $co = $db->simpleQuerySingle('SELECT * FROM cotisations WHERE id = ?;',
+ true, (int)$data['id_cotisation']);
+
+ $this->_checkFields($data, !empty($co['id_categorie_compta']));
+
+ $check = $db->simpleQuerySingle('SELECT 1 FROM cotisations_membres
+ WHERE id_cotisation = ? AND id_membre = ? AND date = ?;',
+ false, (int)$data['id_cotisation'], (int)$data['id_membre'], $data['date']);
+
+ if ($check)
+ {
+ throw new UserException('Cette cotisation a déjà été enregistrée pour ce jour-ci et ce membre-ci.');
+ }
+
+ $db->begin();
+
+ $db->simpleInsert('cotisations_membres', [
+ 'date' => $data['date'],
+ 'id_cotisation' => $data['id_cotisation'],
+ 'id_membre' => $data['id_membre'],
+ ]);
+
+ $id = $db->lastInsertRowId();
+
+ if ($co['id_categorie_compta'] && $co['montant'] > 0)
+ {
+ try {
+ $id_operation = $this->addOperationCompta($id, [
+ 'id_categorie' => $co['id_categorie_compta'],
+ 'libelle' => 'Cotisation (automatique)',
+ 'montant' => $data['montant'],
+ 'date' => $data['date'],
+ 'moyen_paiement'=> $data['moyen_paiement'],
+ 'numero_cheque' => isset($data['numero_cheque']) ? $data['numero_cheque'] : null,
+ 'id_auteur' => $data['id_auteur'],
+ 'banque' => isset($data['banque']) ? $data['banque'] : null,
+ 'id_membre' => $data['id_membre'],
+ ]);
+ }
+ catch (\Exception $e)
+ {
+ $db->rollback();
+ throw $e;
+ }
+ }
+
+ $db->commit();
+
+ return $id;
+ }
+
+ /**
+ * Supprimer un événement de cotisation
+ * @param integer $id ID de l'événement à supprimer
+ * @return integer true en cas de succès
+ */
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+ $db->simpleExec('DELETE FROM membres_operations WHERE id_cotisation = ?;', (int)$id);
+ return $db->simpleExec('DELETE FROM cotisations_membres WHERE id = ?;', (int) $id);
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT * FROM cotisations_membres WHERE id = ?;', true, (int)$id);
+ }
+
+ /**
+ * Renvoie une liste des écritures comptables liées à une cotisation
+ * @param int $id Numéro de la cotisation membre
+ * @return array Liste des écritures
+ */
+ public function listOperationsCompta($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT * FROM compta_journal
+ WHERE id IN (SELECT id_operation FROM membres_operations
+ WHERE id_cotisation = ?);', \SQLITE3_ASSOC, (int)$id);
+ }
+
+ /**
+ * Ajouter une écriture comptable pour un paiemement membre
+ * @param int $id Numéro de la cotisation membre
+ * @param array $data Données
+ */
+ public function addOperationCompta($id, $data)
+ {
+ $journal = new Compta_Journal;
+ $db = DB::getInstance();
+
+ if (!isset($data['libelle']) || trim($data['libelle']) == '')
+ {
+ throw new UserException('Le libellé ne peut rester vide.');
+ }
+
+ $data['libelle'] = trim($data['libelle']);
+
+ if (!isset($data['montant']) || !is_numeric($data['montant']) || (float)$data['montant'] < 0)
+ {
+ throw new UserException('Le montant doit être un nombre positif et valide.');
+ }
+
+ $data['montant'] = (float) $data['montant'];
+
+ if ($data['moyen_paiement'] != 'ES')
+ {
+ $debit = $data['banque'];
+ }
+ else
+ {
+ $debit = Compta_Comptes::CAISSE;
+ }
+
+ $credit = $db->simpleQuerySingle('SELECT compte FROM compta_categories WHERE id = ?;',
+ false, $data['id_categorie']);
+
+ $id_operation = $journal->add([
+ 'libelle' => $data['libelle'],
+ 'montant' => $data['montant'],
+ 'date' => $data['date'],
+ 'moyen_paiement'=> $data['moyen_paiement'],
+ 'numero_cheque' => isset($data['numero_cheque']) ? $data['numero_cheque'] : null,
+ 'compte_debit' => $debit,
+ 'compte_credit' => $credit,
+ 'id_categorie' => (int)$data['id_categorie'],
+ 'id_auteur' => (int)$data['id_auteur'],
+ ]);
+
+ $db->simpleInsert('membres_operations', [
+ 'id_operation' => $id_operation,
+ 'id_membre' => $data['id_membre'],
+ 'id_cotisation' => (int)$id,
+ ]);
+
+ return $id_operation;
+ }
+
+ /**
+ * Nombre de membres pour une cotisation
+ * @param integer $id Numéro de la cotisation
+ * @return integer Nombre d'événements pour cette cotisation
+ */
+ public function countMembersForCotisation($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT COUNT(DISTINCT id_membre) FROM cotisations_membres
+ WHERE id_cotisation = ?;',
+ false, (int)$id);
+ }
+
+ /**
+ * Liste des membres qui sont inscrits à une cotisation
+ * @param integer $id Numéro de la cotisation
+ * @return array Liste des membres
+ */
+ public function listMembersForCotisation($id, $page = 1, $order = null, $desc = true)
+ {
+ $begin = ($page - 1) * self::ITEMS_PER_PAGE;
+
+ $db = DB::getInstance();
+ $champ_id = Config::getInstance()->get('champ_identite');
+
+ if (empty($order))
+ $order = 'date';
+
+ switch ($order)
+ {
+ case 'date':
+ case 'a_jour':
+ break;
+ case 'identite':
+ $order = 'transliterate_to_ascii('.$champ_id.') COLLATE NOCASE';
+ break;
+ default:
+ $order = 'cm.id_membre';
+ break;
+ }
+
+ $desc = $desc ? 'DESC' : 'ASC';
+
+ return $db->simpleStatementFetch('SELECT cm.id_membre, cm.date, cm.id,
+ (SELECT '.$champ_id.' FROM membres WHERE id = cm.id_membre) AS nom, c.montant,
+ CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date()
+ WHEN c.fin IS NOT NULL THEN c.fin >= date() ELSE 1 END AS a_jour
+ FROM cotisations_membres AS cm
+ INNER JOIN cotisations AS c ON c.id = cm.id_cotisation
+ WHERE
+ cm.id_cotisation = ?
+ GROUP BY cm.id_membre ORDER BY '.$order.' '.$desc.' LIMIT ?,?;',
+ \SQLITE3_ASSOC, (int)$id, $begin, self::ITEMS_PER_PAGE);
+ }
+
+ /**
+ * Liste des événements d'un membre
+ * @param integer $id Numéro de membre
+ * @return array Liste des événements de cotisation fait par ce membre
+ */
+ public function listForMember($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT cm.*, c.intitule, c.duree, c.debut, c.fin, c.montant,
+ (SELECT COUNT(*) FROM membres_operations WHERE id_cotisation = cm.id) AS nb_operations
+ FROM cotisations_membres AS cm
+ LEFT JOIN cotisations AS c ON c.id = cm.id_cotisation
+ WHERE cm.id_membre = ? ORDER BY cm.date DESC;', \SQLITE3_ASSOC, (int)$id);
+ }
+
+ /**
+ * Liste des cotisations / activités en cours pour ce membre
+ * @param integer $id Numéro de membre
+ * @return array Liste des cotisations en cours de validité
+ */
+ public function listSubscriptionsForMember($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT c.*,
+ CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date()
+ WHEN c.fin IS NOT NULL THEN c.fin >= date()
+ WHEN cm.id IS NOT NULL THEN 1 ELSE 0 END AS a_jour,
+ CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
+ WHEN c.fin IS NOT NULL THEN c.fin ELSE 1 END AS expiration,
+ (julianday(date()) - julianday(CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
+ WHEN c.fin IS NOT NULL THEN c.fin END)) AS nb_jours
+ FROM cotisations_membres AS cm
+ INNER JOIN cotisations AS c ON c.id = cm.id_cotisation
+ WHERE cm.id_membre = ?
+ GROUP BY cm.id_cotisation
+ ORDER BY cm.date DESC;', \SQLITE3_ASSOC, (int)$id);
+ }
+
+ /**
+ * Ce membre est-il à jour sur cette cotisation ?
+ * @param integer $id Numéro de membre
+ * @param integer $id_cotisation Numéro de cotisation
+ * @return array Infos sur la cotisation, et champ expiration
+ * (si NULL = cotisation jamais enregistrée, si 1 = cotisation ponctuelle enregistrée, sinon date d'expiration)
+ */
+ public function isMemberUpToDate($id, $id_cotisation)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT c.*,
+ CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\') >= date()
+ WHEN c.fin IS NOT NULL THEN c.fin >= date()
+ WHEN cm.id IS NOT NULL THEN 1 ELSE 0 END AS a_jour,
+ CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
+ WHEN c.fin IS NOT NULL THEN c.fin ELSE 1 END AS expiration
+ FROM cotisations AS c
+ LEFT JOIN cotisations_membres AS cm ON cm.id_cotisation = c.id AND cm.id_membre = ?
+ WHERE c.id = ? ORDER BY cm.date DESC;',
+ true, (int)$id, (int)$id_cotisation);
+ }
+
+ public function countForMember($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT COUNT(DISTINCT id_cotisation) FROM cotisations_membres
+ WHERE id_membre = ?;', false, (int)$id);
+ }
+}
\ No newline at end of file
diff --git a/include/class.db.php b/include/class.db.php
new file mode 100644
index 0000000..c560c59
--- /dev/null
+++ b/include/class.db.php
@@ -0,0 +1,411 @@
+enableExceptions(true);
+
+ // Activer les contraintes des foreign keys
+ $this->exec('PRAGMA foreign_keys = ON;');
+
+ $this->createFunction('transliterate_to_ascii', ['Garradin\utils', 'transliterateToAscii']);
+ $this->createFunction('base64', 'base64_encode');
+ $this->createFunction('rank', [$this, 'sql_rank']);
+ $this->createFunction('running_sum', [$this, 'sql_running_sum']);
+ }
+
+ public function sql_running_sum($data)
+ {
+ // Why is this function called two times for the first row?!
+ // Dunno but here is a workaround
+ if (is_null($this->_running_sum))
+ {
+ $this->_running_sum = 0.0;
+ return $this->_running_sum;
+ }
+
+ $this->_running_sum += $data;
+ return $this->_running_sum;
+ }
+
+ public function resetRunningSum()
+ {
+ $this->_running_sum = null;
+ }
+
+ public function sql_rank($aMatchInfo)
+ {
+ $iSize = 4; // byte size
+ $iPhrase = (int) 0; // Current phrase //
+ $score = (double)0.0; // Value to return //
+
+ /* Check that the number of arguments passed to this function is correct.
+ ** If not, jump to wrong_number_args. Set aMatchinfo to point to the array
+ ** of unsigned integer values returned by FTS function matchinfo. Set
+ ** nPhrase to contain the number of reportable phrases in the users full-text
+ ** query, and nCol to the number of columns in the table.
+ */
+ $aMatchInfo = (string) func_get_arg(0);
+ $nPhrase = ord(substr($aMatchInfo, 0, $iSize));
+ $nCol = ord(substr($aMatchInfo, $iSize, $iSize));
+
+ if (func_num_args() > (1 + $nCol))
+ {
+ throw new \Exception("Invalid number of arguments : ".$nCol);
+ }
+
+ // Iterate through each phrase in the users query. //
+ for ($iPhrase = 0; $iPhrase < $nPhrase; $iPhrase++)
+ {
+ $iCol = (int) 0; // Current column //
+
+ /* Now iterate through each column in the users query. For each column,
+ ** increment the relevancy score by:
+ **
+ ** ( / ) *
+ **
+ ** aPhraseinfo[] points to the start of the data for phrase iPhrase. So
+ ** the hit count and global hit counts for each column are found in
+ ** aPhraseinfo[iCol*3] and aPhraseinfo[iCol*3+1], respectively.
+ */
+ $aPhraseinfo = substr($aMatchInfo, (2 + $iPhrase * $nCol * 3) * $iSize);
+
+ for ($iCol = 0; $iCol < $nCol; $iCol++)
+ {
+ $nHitCount = ord(substr($aPhraseinfo, 3 * $iCol * $iSize, $iSize));
+ $nGlobalHitCount = ord(substr($aPhraseinfo, (3 * $iCol + 1) * $iSize, $iSize));
+ $weight = ($iCol < func_num_args() - 1) ? (double) func_get_arg($iCol + 1) : 0;
+
+ if ($nHitCount > 0)
+ {
+ $score += ((double)$nHitCount / (double)$nGlobalHitCount) * $weight;
+ }
+ }
+ }
+
+ return $score;
+ }
+
+ public function escape($str)
+ {
+ return $this->escapeString($str);
+ }
+
+ public function e($str)
+ {
+ return $this->escapeString($str);
+ }
+
+ public function begin()
+ {
+ if (!$this->_transaction)
+ {
+ $this->exec('BEGIN;');
+ }
+
+ $this->_transaction++;
+
+ return $this->_transaction == 1 ? true : false;
+ }
+
+ public function commit()
+ {
+ if ($this->_transaction == 1)
+ {
+ $this->exec('END;');
+ }
+
+ if ($this->_transaction > 0)
+ {
+ $this->_transaction--;
+ }
+
+ return $this->_transaction ? false : true;
+ }
+
+ public function rollback()
+ {
+ $this->exec('ROLLBACK;');
+ $this->_transaction = 0;
+ return true;
+ }
+
+ protected function _getArgType($arg, $name = '')
+ {
+ if (is_float($arg))
+ return SQLITE3_FLOAT;
+ elseif (is_int($arg))
+ return SQLITE3_INTEGER;
+ elseif (is_bool($arg))
+ return SQLITE3_INTEGER;
+ elseif (is_null($arg))
+ return SQLITE3_NULL;
+ elseif (is_string($arg))
+ return SQLITE3_TEXT;
+ else
+ throw new \InvalidArgumentException('Argument '.$name.' is of invalid type '.gettype($arg));
+ }
+
+ public function simpleStatement($query, $args = [])
+ {
+ $statement = $this->prepare($query);
+ $nb = $statement->paramCount();
+
+ if (!empty($args))
+ {
+ if (is_array($args) && count($args) == 1 && is_array(current($args)))
+ {
+ $args = current($args);
+ }
+
+ if (count($args) != $nb)
+ {
+ throw new \LengthException('Arguments error: '.count($args).' supplied, but '.$nb.' are required by query.');
+ }
+
+ reset($args);
+
+ if (is_int(key($args)))
+ {
+ foreach ($args as $i=>$arg)
+ {
+ $statement->bindValue((int)$i+1, $arg, $this->_getArgType($arg, $i+1));
+ }
+ }
+ else
+ {
+ foreach ($args as $key=>$value)
+ {
+ if (is_int($key))
+ {
+ throw new \InvalidArgumentException(__FUNCTION__ . ' requires argument to be a named-associative array, but key '.$key.' is an integer.');
+ }
+
+ $statement->bindValue(':'.$key, $value, $this->_getArgType($value, $key));
+ }
+ }
+ }
+
+ try {
+ return $statement->execute();
+ }
+ catch (\Exception $e)
+ {
+ throw new \Exception($e->getMessage() . "\n" . $query . "\n" . json_encode($args, true));
+ }
+ }
+
+ public function simpleStatementFetch($query, $mode = SQLITE3_BOTH)
+ {
+ if ($mode != SQLITE3_BOTH && $mode != SQLITE3_ASSOC && $mode != SQLITE3_NUM)
+ {
+ throw new \InvalidArgumentException('Mode argument should be either SQLITE3_BOTH, SQLITE3_ASSOC or SQLITE3_NUM.');
+ }
+
+ $args = array_slice(func_get_args(), 2);
+ return $this->fetchResult($this->simpleStatement($query, $args), $mode);
+ }
+
+ public function simpleStatementFetchAssoc($query)
+ {
+ $args = array_slice(func_get_args(), 1);
+ return $this->fetchResultAssoc($this->simpleStatement($query, $args));
+ }
+
+ public function simpleStatementFetchAssocKey($query, $mode = SQLITE3_BOTH)
+ {
+ if ($mode != SQLITE3_BOTH && $mode != SQLITE3_ASSOC && $mode != SQLITE3_NUM)
+ {
+ throw new \InvalidArgumentException('Mode argument should be either SQLITE3_BOTH, SQLITE3_ASSOC or SQLITE3_NUM.');
+ }
+
+ $args = array_slice(func_get_args(), 2);
+ return $this->fetchResultAssocKey($this->simpleStatement($query, $args), $mode);
+ }
+
+ public function escapeAuto($value, $name = '')
+ {
+ $type = $this->_getArgType($value, $name);
+
+ switch ($type)
+ {
+ case SQLITE3_FLOAT:
+ return floatval($value);
+ case SQLITE3_INTEGER:
+ return intval($value);
+ case SQLITE3_NULL:
+ return 'NULL';
+ case SQLITE3_TEXT:
+ return '\'' . $this->escapeString($value) . '\'';
+ }
+ }
+
+ /**
+ * Simple INSERT query
+ */
+ public function simpleInsert($table, $fields)
+ {
+ $fields_names = array_keys($fields);
+ return $this->simpleStatement('INSERT INTO '.$table.' ('.implode(', ', $fields_names).')
+ VALUES (:'.implode(', :', $fields_names).');', $fields);
+ }
+
+ public function simpleUpdate($table, $fields, $where)
+ {
+ if (empty($fields))
+ return false;
+
+ $query = 'UPDATE '.$table.' SET ';
+
+ foreach ($fields as $key=>$value)
+ {
+ $query .= $key . ' = :'.$key.', ';
+ }
+
+ $query = substr($query, 0, -2);
+ $query .= ' WHERE '.$where.';';
+ return $this->simpleStatement($query, $fields);
+ }
+
+ /**
+ * Formats and escapes a statement and then returns the result of exec()
+ */
+ public function simpleExec($query)
+ {
+ return $this->simpleStatement($query, array_slice(func_get_args(), 1));
+ }
+
+ public function simpleQuerySingle($query, $all_columns = false)
+ {
+ $res = $this->simpleStatement($query, array_slice(func_get_args(), 2));
+
+ $row = $res->fetchArray($all_columns ? SQLITE3_ASSOC : SQLITE3_NUM);
+
+ if (!$all_columns)
+ {
+ if (isset($row[0]))
+ return $row[0];
+ return false;
+ }
+ else
+ {
+ return $row;
+ }
+ }
+
+ public function queryFetch($query, $mode = SQLITE3_BOTH)
+ {
+ return $this->fetchResult($this->query($query), $mode);
+ }
+
+ public function queryFetchAssoc($query)
+ {
+ return $this->fetchResultAssoc($this->query($query));
+ }
+
+ public function queryFetchAssocKey($query, $mode = SQLITE3_BOTH)
+ {
+ return $this->fetchResultAssocKey($this->query($query), $mode);
+ }
+
+ public function fetchResult($result, $mode = \SQLITE3_BOTH)
+ {
+ $out = [];
+
+ while ($row = $result->fetchArray($mode))
+ {
+ $out[] = $row;
+ }
+
+ $result->finalize();
+ unset($result, $row);
+
+ return $out;
+ }
+
+ protected function fetchResultAssoc($result)
+ {
+ $out = [];
+
+ while ($row = $result->fetchArray(SQLITE3_NUM))
+ {
+ $out[$row[0]] = $row[1];
+ }
+
+ $result->finalize();
+ unset($result, $row);
+
+ return $out;
+ }
+
+ protected function fetchResultAssocKey($result, $mode = \SQLITE3_BOTH)
+ {
+ $out = [];
+
+ while ($row = $result->fetchArray($mode))
+ {
+ $key = current($row);
+ $out[$key] = $row;
+ }
+
+ $result->finalize();
+ unset($result, $row);
+
+ return $out;
+ }
+
+ public function countRows($result)
+ {
+ $i = 0;
+
+ while ($result->fetchArray(SQLITE3_NUM))
+ $i++;
+
+ return $i;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.membres.php b/include/class.membres.php
new file mode 100644
index 0000000..0136d6d
--- /dev/null
+++ b/include/class.membres.php
@@ -0,0 +1,792 @@
+_getSalt(22);
+ return crypt($password, $salt);
+ }
+
+ protected function _checkPassword($password, $stored_hash)
+ {
+ return crypt($password, $stored_hash) == $stored_hash;
+ }
+
+ protected function _sessionStart($force = false)
+ {
+ if (!isset($_SESSION) && ($force || isset($_COOKIE[session_name()])))
+ {
+ session_start();
+ }
+
+ return true;
+ }
+
+ public function keepSessionAlive()
+ {
+ $this->_sessionStart(true);
+ }
+
+ public function login($id, $passe)
+ {
+ $db = DB::getInstance();
+ $champ_id = Config::getInstance()->get('champ_identifiant');
+
+ $r = $db->simpleQuerySingle('SELECT id, passe, id_categorie FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));
+
+ if (empty($r))
+ return false;
+
+ if (!$this->_checkPassword(trim($passe), $r['passe']))
+ return false;
+
+ $droits = $this->getDroits($r['id_categorie']);
+
+ if ($droits['connexion'] == self::DROIT_AUCUN)
+ return false;
+
+ $this->_sessionStart(true);
+ $db->simpleExec('UPDATE membres SET date_connexion = datetime(\'now\') WHERE id = ?;', $r['id']);
+
+ return $this->updateSessionData($r['id'], $droits);
+ }
+
+ public function recoverPasswordCheck($id)
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ $champ_id = $config->get('champ_identifiant');
+
+ $membre = $db->simpleQuerySingle('SELECT id, email FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));
+
+ if (!$membre || trim($membre['email']) == '')
+ {
+ return false;
+ }
+
+ $this->_sessionStart(true);
+ $hash = sha1($membre['email'] . $membre['id'] . 'recover' . ROOT . time());
+ $_SESSION['recover_password'] = [
+ 'id' => (int) $membre['id'],
+ 'email' => $membre['email'],
+ 'hash' => $hash
+ ];
+
+ $message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n";
+ $message.= "Il vous suffit de cliquer sur le lien ci-dessous pour recevoir un nouveau mot de passe.\n\n";
+ $message.= WWW_URL . 'admin/password.php?c=' . substr($hash, -10);
+ $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé.";
+
+ return utils::mail($membre['email'], '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message);
+ }
+
+ public function recoverPasswordConfirm($hash)
+ {
+ $this->_sessionStart();
+
+ if (empty($_SESSION['recover_password']['hash']))
+ return false;
+
+ if (substr($_SESSION['recover_password']['hash'], -10) != $hash)
+ return false;
+
+ $config = Config::getInstance();
+ $db = DB::getInstance();
+
+ $password = utils::suggestPassword();
+
+ $dest = $_SESSION['recover_password']['email'];
+ $id = (int)$_SESSION['recover_password']['id'];
+
+ $message = "Bonjour,\n\nVous avez demandé un nouveau mot de passe pour votre compte.\n\n";
+ $message.= "Votre adresse email : ".$dest."\n";
+ $message.= "Votre nouveau mot de passe : ".$password."\n\n";
+ $message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler.";
+
+ $password = $this->_hashPassword($password);
+
+ $db->simpleUpdate('membres', ['passe' => $password], 'id = '.(int)$id);
+
+ return utils::mail($dest, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message);
+ }
+
+ public function updateSessionData($membre = null, $droits = null)
+ {
+ if (is_null($membre))
+ {
+ $membre = $this->get($_SESSION['logged_user']['id']);
+ }
+ elseif (is_int($membre))
+ {
+ $membre = $this->get($membre);
+ }
+
+ if (is_null($droits))
+ {
+ $droits = $this->getDroits($membre['id_categorie']);
+ }
+
+ $membre['droits'] = $droits;
+ $_SESSION['logged_user'] = $membre;
+ return true;
+ }
+
+ public function localLogin()
+ {
+ if (!defined('Garradin\LOCAL_LOGIN'))
+ return false;
+
+ if (trim(LOCAL_LOGIN) == '')
+ return false;
+
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+ $champ_id = $config->get('champ_identifiant');
+
+ if (is_int(LOCAL_LOGIN) && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ? LIMIT 1;', true, LOCAL_LOGIN))
+ {
+ $this->_sessionStart(true);
+ return $this->updateSessionData(LOCAL_LOGIN);
+ }
+ elseif ($id = $db->simpleQuerySingle('SELECT id FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, LOCAL_LOGIN))
+ {
+ $this->_sessionStart(true);
+ return $this->updateSessionData($membre);
+ }
+
+ throw new UserException('Le membre ' . LOCAL_LOGIN . ' n\'existe pas, merci de modifier la directive Garradin\LOCAL_LOGIN.');
+ }
+
+ public function isLogged()
+ {
+ $this->_sessionStart();
+
+ if (empty($_SESSION['logged_user']))
+ {
+ if (defined('Garradin\LOCAL_LOGIN'))
+ {
+ return $this->localLogin();
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getLoggedUser()
+ {
+ if (!$this->isLogged())
+ return false;
+
+ return $_SESSION['logged_user'];
+ }
+
+ public function logout()
+ {
+ $_SESSION = [];
+ setcookie(session_name(), '', 0, '/');
+ return true;
+ }
+
+ public function sessionStore($key, $value)
+ {
+ if (!isset($_SESSION['storage']))
+ {
+ $_SESSION['storage'] = [];
+ }
+
+ if ($value === null)
+ {
+ unset($_SESSION['storage'][$key]);
+ }
+ else
+ {
+ $_SESSION['storage'][$key] = $value;
+ }
+
+ return true;
+ }
+
+ public function sessionGet($key)
+ {
+ if (!isset($_SESSION['storage'][$key]))
+ {
+ return null;
+ }
+
+ return $_SESSION['storage'][$key];
+ }
+
+ public function sendMessage($dest, $sujet, $message, $copie = false)
+ {
+ if (!$this->isLogged())
+ {
+ throw new \LogicException('Cette fonction ne peut être appelée que par un utilisateur connecté.');
+ }
+
+ $from = $this->getLoggedUser();
+ $from = $from['email'];
+ // Uniquement adresse email pour le moment car faudrait trouver comment
+ // indiquer le nom mais qu'il soit correctement échappé FIXME
+
+ $config = Config::getInstance();
+
+ $message .= "\n\n--\nCe message a été envoyé par un membre de ".$config->get('nom_asso');
+ $message .= ", merci de contacter ".$config->get('email_asso')." en cas d'abus.";
+
+ if ($copie)
+ {
+ utils::mail($from, $sujet, $message);
+ }
+
+ return utils::mail($dest, $sujet, $message, ['From' => $from]);
+ }
+
+ // Gestion des données ///////////////////////////////////////////////////////
+
+ public function _checkFields(&$data, $check_editable = true, $check_password = true)
+ {
+ $champs = Config::getInstance()->get('champs_membres');
+
+ foreach ($champs->getAll() as $key=>$config)
+ {
+ if (!$check_editable && (!empty($config['private']) || empty($config['editable'])))
+ {
+ unset($data[$key]);
+ continue;
+ }
+
+ if (!isset($data[$key]) || (!is_array($data[$key]) && trim($data[$key]) === '')
+ || (is_array($data[$key]) && empty($data[$key])))
+ {
+ if (!empty($config['mandatory']) && ($check_password || $key != 'passe'))
+ {
+ throw new UserException('Le champ "' . $config['title'] . '" doit obligatoirement être renseigné.');
+ }
+ elseif (!empty($config['mandatory']))
+ {
+ continue;
+ }
+ }
+
+ if (isset($data[$key]))
+ {
+ if ($config['type'] == 'email' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_EMAIL))
+ {
+ throw new UserException('Adresse e-mail invalide dans le champ "' . $config['title'] . '".');
+ }
+ elseif ($config['type'] == 'url' && trim($data[$key]) !== '' && !filter_var($data[$key], FILTER_VALIDATE_URL))
+ {
+ throw new UserException('Adresse URL invalide dans le champ "' . $config['title'] . '".');
+ }
+ elseif ($config['type'] == 'date' && trim($data[$key]) !== '' && !utils::checkDate($data[$key]))
+ {
+ throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ.');
+ }
+ elseif ($config['type'] == 'datetime' && trim($data[$key]) !== '')
+ {
+ if (!utils::checkDateTime($data[$key]) || !($dt = new DateTime($data[$key])))
+ {
+ throw new UserException('Date invalide "' . $config['title'] . '", format attendu : AAAA-MM-JJ HH:mm.');
+ }
+
+ $data[$key] = $dt->format('Y-m-d H:i');
+ }
+ elseif ($config['type'] == 'tel')
+ {
+ $data[$key] = utils::normalizePhoneNumber($data[$key]);
+ }
+ elseif ($config['type'] == 'country')
+ {
+ $data[$key] = strtoupper(substr($data[$key], 0, 2));
+ }
+ elseif ($config['type'] == 'checkbox')
+ {
+ $data[$key] = empty($data[$key]) ? 0 : 1;
+ }
+ elseif ($config['type'] == 'number' && trim($data[$key]) !== '')
+ {
+ if (empty($data[$key]))
+ {
+ $data[$key] = 0;
+ }
+
+ if (!is_numeric($data[$key]))
+ throw new UserException('Le champ "' . $config['title'] . '" doit contenir un chiffre.');
+ }
+ elseif ($config['type'] == 'select' && !in_array($data[$key], $config['options']))
+ {
+ throw new UserException('Le champ "' . $config['title'] . '" ne correspond pas à un des choix proposés.');
+ }
+ elseif ($config['type'] == 'multiple')
+ {
+ if (empty($data[$key]) || !is_array($data[$key]))
+ {
+ $data[$key] = 0;
+ continue;
+ }
+
+ $binary = 0;
+
+ foreach ($data[$key] as $k => $v)
+ {
+ if (array_key_exists($k, $config['options']) && !empty($v))
+ {
+ $binary |= 0x01 << $k;
+ }
+ }
+
+ $data[$key] = $binary;
+ }
+
+ // Un champ texte vide c'est un champ NULL
+ if (is_string($data[$key]) && trim($data[$key]) === '')
+ {
+ $data[$key] = null;
+ }
+ }
+ }
+
+ if (isset($data['code_postal']) && trim($data['code_postal']) != '')
+ {
+ if (!empty($data['pays']) && $data['pays'] == 'FR' && !preg_match('!^\d{5}$!', $data['code_postal']))
+ {
+ throw new UserException('Code postal invalide.');
+ }
+ }
+
+ if (!empty($data['passe']) && strlen($data['passe']) < 5)
+ {
+ throw new UserException('Le mot de passe doit faire au moins 5 caractères.');
+ }
+
+ return true;
+ }
+
+ public function add($data = [])
+ {
+ $this->_checkFields($data);
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+ $id = $config->get('champ_identifiant');
+
+ if (!empty($data[$id])
+ && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE '.$id.' = ? LIMIT 1;', false, $data[$id]))
+ {
+ throw new UserException('La valeur du champ '.$id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.');
+ }
+
+ if (isset($data['passe']) && trim($data['passe']) != '')
+ {
+ $data['passe'] = $this->_hashPassword($data['passe']);
+ }
+ else
+ {
+ unset($data['passe']);
+ }
+
+ if (empty($data['id_categorie']))
+ {
+ $data['id_categorie'] = Config::getInstance()->get('categorie_membres');
+ }
+
+ $db->simpleInsert('membres', $data);
+ return $db->lastInsertRowId();
+ }
+
+ public function edit($id, $data = [], $check_editable = true)
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ if (isset($data['id']) && ($data['id'] == $id || empty($data['id'])))
+ {
+ unset($data['id']);
+ }
+
+ $this->_checkFields($data, $check_editable, false);
+ $champ_id = $config->get('champ_identifiant');
+
+ if (!empty($data[$champ_id])
+ && $db->simpleQuerySingle('SELECT 1 FROM membres WHERE '.$champ_id.' = ? AND id != ? LIMIT 1;', false, $data[$champ_id], (int)$id))
+ {
+ throw new UserException('La valeur du champ '.$champ_id.' est déjà utilisée par un autre membre, hors ce champ doit être unique à chaque membre.');
+ }
+
+ if (!empty($data['id']))
+ {
+ if ($db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int)$data['id']))
+ {
+ throw new UserException('Ce numéro est déjà attribué à un autre membre.');
+ }
+
+ // Si on ne vérifie pas toutes les tables qui sont liées ici à un ID de membre
+ // la requête de modification provoquera une erreur de contrainte de foreign key
+ // ce qui est normal. Donc : il n'est pas possible de changer l'ID d'un membre qui
+ // a participé au wiki, à la compta, etc.
+ if ($db->simpleQuerySingle('SELECT 1 FROM wiki_revisions WHERE id_auteur = ?;', false, (int)$id)
+ || $db->simpleQuerySingle('SELECT 1 FROM compta_journal WHERE id_auteur = ?;', false, (int)$id))
+ # FIXME || $db->simpleQuerySingle('SELECT 1 FROM wiki_suivi WHERE id_membre = ?;', false, (int)$id))
+ {
+ throw new UserException('Le numéro n\'est pas modifiable pour ce membre car des contenus sont liés à ce numéro de membre (wiki, compta, etc.).');
+ }
+ }
+
+ if (!empty($data['passe']) && trim($data['passe']))
+ {
+ $data['passe'] = $this->_hashPassword($data['passe']);
+ }
+ else
+ {
+ unset($data['passe']);
+ }
+
+ if (isset($data['id_categorie']) && empty($data['id_categorie']))
+ {
+ $data['id_categorie'] = Config::getInstance()->get('categorie_membres');
+ }
+
+ if (empty($data))
+ {
+ return true;
+ }
+
+ return $db->simpleUpdate('membres', $data, 'id = '.(int)$id);
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ return $db->simpleQuerySingle('SELECT *,
+ '.$config->get('champ_identite').' AS identite,
+ strftime(\'%s\', date_inscription) AS date_inscription,
+ strftime(\'%s\', date_connexion) AS date_connexion
+ FROM membres WHERE id = ? LIMIT 1;', true, (int)$id);
+ }
+
+ public function delete($ids)
+ {
+ if (!is_array($ids))
+ {
+ $ids = [(int)$ids];
+ }
+
+ if ($this->isLogged())
+ {
+ $user = $this->getLoggedUser();
+
+ foreach ($ids as $id)
+ {
+ if ($user['id'] == $id)
+ {
+ throw new UserException('Il n\'est pas possible de supprimer son propre compte.');
+ }
+ }
+ }
+
+ return self::_deleteMembres($ids);
+ }
+
+ public function getNom($id)
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ return $db->simpleQuerySingle('SELECT '.$config->get('champ_identite').' FROM membres WHERE id = ? LIMIT 1;', false, (int)$id);
+ }
+
+ public function getDroits($id)
+ {
+ $db = DB::getInstance();
+ $droits = $db->simpleQuerySingle('SELECT * FROM membres_categories WHERE id = ?;', true, (int)$id);
+
+ foreach ($droits as $key=>$value)
+ {
+ unset($droits[$key]);
+ $key = str_replace('droit_', '', $key, $found);
+
+ if ($found)
+ {
+ $droits[$key] = (int) $value;
+ }
+ }
+
+ return $droits;
+ }
+
+ public function search($field, $query)
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ $champs = $config->get('champs_membres');
+
+ if ($field != 'id' && !$champs->get($field))
+ {
+ throw new \UnexpectedValueException($field . ' is not a valid field');
+ }
+
+ $champ = $champs->get($field);
+
+ if ($champ['type'] == 'multiple')
+ {
+ $where = 'WHERE '.$field.' & (1 << '.(int)$query.')';
+ $order = false;
+ }
+ elseif ($champ['type'] == 'tel')
+ {
+ $query = utils::normalizePhoneNumber($query);
+ $query = preg_replace('!^0+!', '', $query);
+
+ if ($query == '')
+ {
+ return false;
+ }
+
+ $where = 'WHERE '.$field.' LIKE \'%'.$db->escapeString($query).'\'';
+ $order = $field;
+ }
+ elseif (!$champs->isText($field))
+ {
+ $where = 'WHERE '.$field.' = \''.$db->escapeString($query).'\'';
+ $order = $field;
+ }
+ else
+ {
+ $where = 'WHERE transliterate_to_ascii('.$field.') LIKE transliterate_to_ascii(\'%'.$db->escapeString($query).'%\')';
+ $order = 'transliterate_to_ascii('.$field.') COLLATE NOCASE';
+ }
+
+ $fields = array_keys($champs->getListedFields());
+
+ if (!in_array($field, $fields))
+ {
+ $fields[] = $field;
+ }
+
+ if (!in_array('email', $fields))
+ {
+ $fields[] = 'email';
+ }
+
+ return $db->simpleStatementFetch(
+ 'SELECT id, id_categorie, ' . implode(', ', $fields) . ',
+ '.$config->get('champ_identite').' AS identite,
+ strftime(\'%s\', date_inscription) AS date_inscription
+ FROM membres ' . $where . ($order ? ' ORDER BY ' . $order : '') . '
+ LIMIT 1000;',
+ SQLITE3_ASSOC
+ );
+ }
+
+ public function listByCategory($cat, $fields, $page = 1, $order = null, $desc = false)
+ {
+ $begin = ($page - 1) * self::ITEMS_PER_PAGE;
+
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ $champs = $config->get('champs_membres');
+
+ if (is_int($cat) && $cat)
+ $where = 'WHERE id_categorie = '.(int)$cat;
+ elseif (is_array($cat))
+ $where = 'WHERE id_categorie IN ('.implode(',', $cat).')';
+ else
+ $where = '';
+
+ if (is_null($order) || !$champs->get($order))
+ $order = 'id';
+
+ if (!empty($fields) && $order != 'id' && $champs->isText($order))
+ {
+ $order = 'transliterate_to_ascii('.$order.') COLLATE NOCASE';
+ }
+
+ if ($desc)
+ {
+ $order .= ' DESC';
+ }
+
+ if (!in_array('email', $fields))
+ {
+ $fields []= 'email';
+ }
+
+ $fields = implode(', ', $fields);
+
+ $query = 'SELECT id, id_categorie, '.$fields.', '.$config->get('champ_identite').' AS identite,
+ strftime(\'%s\', date_inscription) AS date_inscription
+ FROM membres '.$where.'
+ ORDER BY '.$order.' LIMIT ?, ?;';
+
+ return $db->simpleStatementFetch($query, SQLITE3_ASSOC, $begin, self::ITEMS_PER_PAGE);
+ }
+
+ public function countByCategory($cat = 0)
+ {
+ $db = DB::getInstance();
+
+ if (is_int($cat) && $cat)
+ $where = 'WHERE id_categorie = '.(int)$cat;
+ elseif (is_array($cat))
+ $where = 'WHERE id_categorie IN ('.implode(',', $cat).')';
+ else
+ $where = '';
+
+ return $db->simpleQuerySingle('SELECT COUNT(*) FROM membres '.$where.';');
+ }
+
+ public function countAllButHidden()
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT COUNT(*) FROM membres WHERE id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1);');
+ }
+
+ static public function changeCategorie($id_cat, $membres)
+ {
+ foreach ($membres as &$id)
+ {
+ $id = (int) $id;
+ }
+
+ $db = DB::getInstance();
+ return $db->simpleUpdate('membres',
+ ['id_categorie' => (int)$id_cat],
+ 'id IN ('.implode(',', $membres).')'
+ );
+ }
+
+ static protected function _deleteMembres($membres)
+ {
+ foreach ($membres as &$id)
+ {
+ $id = (int) $id;
+ }
+
+ $membres = implode(',', $membres);
+
+ $db = DB::getInstance();
+ $db->exec('UPDATE wiki_revisions SET id_auteur = NULL WHERE id_auteur IN ('.$membres.');');
+ $db->exec('UPDATE compta_journal SET id_auteur = NULL WHERE id_auteur IN ('.$membres.');');
+ //$db->exec('DELETE FROM wiki_suivi WHERE id_membre IN ('.$membres.');');
+ return $db->exec('DELETE FROM membres WHERE id IN ('.$membres.');');
+ }
+
+ public function sendMessageToCategory($dest, $sujet, $message, $subscribed_only = false)
+ {
+ $config = Config::getInstance();
+
+ $headers = [
+ 'From' => '"'.$config->get('nom_asso').'" <'.$config->get('email_asso').'>',
+ ];
+ $message .= "\n\n--\n".$config->get('nom_asso')."\n".$config->get('site_asso');
+
+ if ($dest == 0)
+ $where = 'id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)';
+ else
+ $where = 'id_categorie = '.(int)$dest;
+
+ if ($subscribed_only)
+ {
+ $where .= ' AND lettre_infos = 1';
+ }
+
+ $db = DB::getInstance();
+ $res = $db->query('SELECT email FROM membres WHERE LENGTH(email) > 0 AND '.$where.' ORDER BY id;');
+
+ $sujet = '['.$config->get('nom_asso').'] '.$sujet;
+
+ while ($row = $res->fetchArray(SQLITE3_ASSOC))
+ {
+ utils::mail($row['email'], $sujet, $message, $headers);
+ }
+
+ return true;
+ }
+
+ public function searchSQL($query)
+ {
+ $db = DB::getInstance();
+
+ $st = $db->prepare($query);
+
+ if (!$st->readOnly())
+ {
+ throw new UserException('Seules les requêtes en lecture sont autorisées.');
+ }
+
+ if (!preg_match('/LIMIT\s+/', $query))
+ {
+ $query = preg_replace('/;?\s*$/', '', $query);
+ $query .= ' LIMIT 100';
+ }
+
+ if (!preg_match('/FROM\s+membres(?:\s+|$|;)/i', $query))
+ {
+ throw new UserException('Seules les requêtes sur la table membres sont autorisées.');
+ }
+
+ if (preg_match('/;\s*(.+?)$/', $query))
+ {
+ throw new UserException('Une seule requête peut être envoyée en même temps.');
+ }
+
+ $st = $db->prepare($query);
+
+ $res = $st->execute();
+ $out = [];
+
+ while ($row = $res->fetchArray(SQLITE3_ASSOC))
+ {
+ if (array_key_exists('passe', $row))
+ {
+ unset($row['passe']);
+ }
+
+ $out[] = $row;
+ }
+
+ return $out;
+ }
+
+ public function schemaSQL()
+ {
+ $db = DB::getInstance();
+
+ $tables = [
+ 'membres' => $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'),
+ 'categories'=> $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'),
+ ];
+
+ return $tables;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.membres_categories.php b/include/class.membres_categories.php
new file mode 100644
index 0000000..4a78e13
--- /dev/null
+++ b/include/class.membres_categories.php
@@ -0,0 +1,150 @@
+ Membres::DROIT_AUCUN,
+ 'connexion' => Membres::DROIT_ACCES,
+ 'membres' => Membres::DROIT_ACCES,
+ 'compta' => Membres::DROIT_ACCES,
+ 'wiki' => Membres::DROIT_ACCES,
+ 'config' => Membres::DROIT_AUCUN,
+ ];
+
+ static public function getDroitsDefaut()
+ {
+ return $this->droits;
+ }
+
+ protected function _checkData(&$data)
+ {
+ $db = DB::getInstance();
+
+ if (!isset($data['nom']) || !trim($data['nom']))
+ {
+ throw new UserException('Le nom de catégorie ne peut rester vide.');
+ }
+
+ if (!empty($data['id_cotisation_obligatoire'])
+ && !$db->simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;',
+ false, (int)$data['id_cotisation_obligatoire']))
+ {
+ throw new UserException('Numéro de cotisation inconnu.');
+ }
+
+ if (isset($data['id_cotisation_obligatoire']) && empty($data['id_cotisation_obligatoire']))
+ {
+ $data['id_cotisation_obligatoire'] = null;
+ }
+ }
+
+ public function add($data)
+ {
+ $this->_checkData($data);
+
+ if (!isset($data['description']))
+ {
+ $data['description'] = '';
+ }
+
+ foreach ($this->droits as $key=>$value)
+ {
+ if (!isset($data['droit_'.$key]))
+ $data['droit_'.$key] = $value;
+ else
+ $data['droit_'.$key] = (int)$data['droit_'.$key];
+ }
+
+ $db = DB::getInstance();
+ $db->simpleInsert('membres_categories', $data);
+
+ return $db->lastInsertRowID();
+ }
+
+ public function edit($id, $data)
+ {
+ $this->_checkData($data);
+
+ foreach ($this->droits as $key=>$value)
+ {
+ if (isset($data['droit_'.$key]))
+ $data['droit_'.$key] = (int)$data['droit_'.$key];
+ }
+
+ if (!isset($data['cacher']) || $data['cacher'] != 1)
+ $data['cacher'] = 0;
+
+ $db = DB::getInstance();
+ return $db->simpleUpdate('membres_categories', $data, 'id = '.(int)$id);
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+
+ return $db->simpleQuerySingle('SELECT * FROM membres_categories WHERE id = ?;',
+ true, (int) $id);
+ }
+
+ public function remove($id)
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ if ($id == $config->get('categorie_membres'))
+ {
+ throw new UserException('Il est interdit de supprimer la catégorie définie par défaut dans la configuration.');
+ }
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM membres WHERE id_categorie = ?;', false, (int)$id))
+ {
+ throw new UserException('La catégorie contient encore des membres, il n\'est pas possible de la supprimer.');
+ }
+
+ $db->simpleUpdate(
+ 'wiki_pages',
+ [
+ 'droit_lecture' => Wiki::LECTURE_NORMAL,
+ 'droit_ecriture' => Wiki::ECRITURE_NORMAL,
+ ],
+ 'droit_lecture = '.(int)$id.' OR droit_ecriture = '.(int)$id
+ );
+
+ return $db->simpleExec('DELETE FROM membres_categories WHERE id = ?;', (int) $id);
+ }
+
+ public function listSimple()
+ {
+ $db = DB::getInstance();
+ return $db->queryFetchAssoc('SELECT id, nom FROM membres_categories ORDER BY nom;');
+ }
+
+ public function listComplete()
+ {
+ $db = DB::getInstance();
+ return $db->queryFetch('SELECT * FROM membres_categories ORDER BY nom;');
+ }
+
+ public function listCompleteWithStats()
+ {
+ $db = DB::getInstance();
+ return $db->queryFetch('SELECT *, (SELECT COUNT(*) FROM membres WHERE id_categorie = membres_categories.id) AS nombre FROM membres_categories ORDER BY nom;');
+ }
+
+
+ public function listHidden()
+ {
+ $db = DB::getInstance();
+ return $db->queryFetchAssoc('SELECT id, nom FROM membres_categories WHERE cacher = 1;');
+ }
+
+ public function listNotHidden()
+ {
+ $db = DB::getInstance();
+ return $db->queryFetchAssoc('SELECT id, nom FROM membres_categories WHERE cacher = 0;');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.membres_import.php b/include/class.membres_import.php
new file mode 100644
index 0000000..eb3342a
--- /dev/null
+++ b/include/class.membres_import.php
@@ -0,0 +1,272 @@
+ 'date_inscription']
+ * @return boolean TRUE en cas de succès
+ */
+ public function fromGalette($path, $translation_table)
+ {
+ if (!file_exists($path) || !is_readable($path))
+ {
+ throw new \RuntimeException('Fichier inconnu : '.$path);
+ }
+
+ $fp = fopen($path, 'r');
+
+ if (!$fp)
+ {
+ return false;
+ }
+
+ $db = DB::getInstance();
+ $db->exec('BEGIN;');
+ $membres = new Membres;
+
+ $columns = array_flip($this->galette_fields);
+
+ $col = function($column) use (&$row, &$columns)
+ {
+ if (!isset($columns[$column]))
+ return null;
+
+ if (!isset($row[$columns[$column]]))
+ return null;
+
+ return $row[$columns[$column]];
+ };
+
+ $line = 0;
+ $delim = utils::find_csv_delim($fp);
+
+ while (!feof($fp))
+ {
+ $row = fgetcsv($fp, 4096, $delim);
+ $line++;
+
+ if (empty($row))
+ {
+ continue;
+ }
+
+ if (count($row) != count($columns))
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
+ }
+
+ $data = [];
+
+ foreach ($translation_table as $galette=>$garradin)
+ {
+ // Champs qu'on ne veut pas importer
+ if (empty($garradin))
+ continue;
+
+ // Concaténer plusieurs champs
+ if (isset($data[$garradin]))
+ $data[$garradin] .= "\n" . $col($galette);
+ else
+ $data[$garradin] = $col($galette);
+ }
+
+ try {
+ $membres->add($data);
+ }
+ catch (UserException $e)
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : ' . $e->getMessage());
+ }
+ }
+
+ $db->exec('END;');
+
+ fclose($fp);
+ return true;
+ }
+
+ /**
+ * Importer un CSV de la liste des membres depuis un export Garradin
+ * @param string $path Chemin vers le CSV
+ * @return boolean TRUE en cas de succès
+ */
+ public function fromCSV($path)
+ {
+ if (!file_exists($path) || !is_readable($path))
+ {
+ throw new \RuntimeException('Fichier inconnu : '.$path);
+ }
+
+ $fp = fopen($path, 'r');
+
+ if (!$fp)
+ {
+ return false;
+ }
+
+ $db = DB::getInstance();
+ $db->exec('BEGIN;');
+ $membres = new Membres;
+
+ // On récupère les champs qu'on peut importer
+ $champs = Config::getInstance()->get('champs_membres')->getAll();
+ $champs = array_keys($champs);
+ $champs[] = 'date_inscription';
+ $champs[] = 'date_connexion';
+ $champs[] = 'id';
+ $champs[] = 'id_categorie';
+
+ $line = 0;
+ $delim = utils::find_csv_delim($fp);
+
+ while (!feof($fp))
+ {
+ $row = fgetcsv($fp, 4096, $delim);
+
+ $line++;
+
+ if (empty($row))
+ {
+ continue;
+ }
+
+ if ($line == 1)
+ {
+ if (is_numeric($row[0]))
+ {
+ throw new UserException('Erreur sur la ligne 1 : devrait contenir l\'en-tête des colonnes.');
+ }
+
+ $columns = array_flip($row);
+ continue;
+ }
+
+ if (count($row) != count($columns))
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : le nombre de colonnes est incorrect.');
+ }
+
+ $data = [];
+
+ foreach ($columns as $name=>$id)
+ {
+ $name = trim($name);
+
+ // Champs qui n'existent pas dans le schéma actuel
+ if (!in_array($name, $champs))
+ continue;
+
+ if (trim($row[$id]) !== '')
+ $data[$name] = $row[$id];
+ }
+
+ if (!empty($data['id']))
+ {
+ $id = (int)$data['id'];
+ unset($data['id']);
+ }
+ else
+ {
+ $id = false;
+ }
+
+ try {
+ if ($id)
+ $membres->edit($id, $data);
+ else
+ $membres->add($data);
+ }
+ catch (UserException $e)
+ {
+ $db->exec('ROLLBACK;');
+ throw new UserException('Erreur sur la ligne ' . $line . ' : ' . $e->getMessage());
+ }
+ }
+
+ $db->exec('END;');
+
+ fclose($fp);
+ return true;
+ }
+
+ public function toCSV()
+ {
+ $db = DB::getInstance();
+
+ $res = $db->prepare('SELECT m.id, c.nom AS categorie, m.* FROM membres AS m
+ LEFT JOIN membres_categories AS c ON m.id_categorie = c.id ORDER BY c.id;')->execute();
+
+ $fp = fopen('php://output', 'w');
+ $header = false;
+
+ while ($row = $res->fetchArray(SQLITE3_ASSOC))
+ {
+ unset($row['passe']);
+
+ if (!$header)
+ {
+ fputcsv($fp, array_keys($row));
+ $header = true;
+ }
+
+ fputcsv($fp, $row);
+ }
+
+ fclose($fp);
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/include/class.plugin.php b/include/class.plugin.php
new file mode 100644
index 0000000..611e59d
--- /dev/null
+++ b/include/class.plugin.php
@@ -0,0 +1,523 @@
+ 'text/css',
+ 'gif' => 'image/gif',
+ 'htm' => 'text/html',
+ 'html' => 'text/html',
+ 'ico' => 'image/x-ico',
+ 'jpe' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'js' => 'application/x-javascript',
+ 'pdf' => 'application/pdf',
+ 'png' => 'image/png',
+ 'swf' => 'application/shockwave-flash',
+ 'xml' => 'text/xml',
+ 'svg' => 'image/svg+xml',
+ ];
+
+ /**
+ * Construire un objet Plugin pour un plugin
+ * @param string $id Identifiant du plugin
+ * @throws UserException Si le plugin n'est pas installé (n'existe pas en DB)
+ */
+ public function __construct($id)
+ {
+ $db = DB::getInstance();
+ $this->plugin = $db->simpleQuerySingle('SELECT * FROM plugins WHERE id = ?;', true, $id);
+
+ if (!$this->plugin)
+ {
+ throw new UserException('Ce plugin n\'existe pas ou n\'est pas installé correctement.');
+ }
+
+ $this->plugin['config'] = json_decode($this->plugin['config'], true);
+
+ if (!is_array($this->plugin['config']))
+ {
+ $this->plugin['config'] = [];
+ }
+
+ $this->id = $id;
+ }
+
+ /**
+ * Renvoie le chemin absolu vers l'archive du plugin
+ * @return string Chemin PHAR vers l'archive
+ */
+ public function path()
+ {
+ return 'phar://' . PLUGINS_ROOT . '/' . $this->id . '.tar.gz';
+ }
+
+ /**
+ * Renvoie une entrée de la configuration ou la configuration complète
+ * @param string $key Clé à rechercher, ou NULL si on désire toutes les entrées de la
+ * @return mixed L'entrée demandée (mixed), ou l'intégralité de la config (array),
+ * ou NULL si l'entrée demandée n'existe pas.
+ */
+ public function getConfig($key = null)
+ {
+ if (is_null($key))
+ {
+ return $this->plugin['config'];
+ }
+
+ if (array_key_exists($key, $this->plugin['config']))
+ {
+ return $this->plugin['config'][$key];
+ }
+
+ return null;
+ }
+
+ /**
+ * Enregistre une entrée dans la configuration du plugin
+ * @param string $key Clé à modifier
+ * @param mixed $value Valeur à enregistrer, choisir NULL pour effacer cette clé de la configuration
+ * @return boolean TRUE si tout se passe bien
+ */
+ public function setConfig($key, $value = null)
+ {
+ if (is_null($value))
+ {
+ unset($this->plugin['config'][$key]);
+ }
+ else
+ {
+ $this->plugin['config'][$key] = $value;
+ }
+
+ $db = DB::getInstance();
+ $db->simpleUpdate('plugins',
+ ['config' => json_encode($this->plugin['config'])],
+ 'id = \'' . $this->id . '\'');
+
+ return true;
+ }
+
+ /**
+ * Renvoie une information ou toutes les informations sur le plugin
+ * @param string $key Clé de l'info à retourner, ou NULL pour recevoir toutes les infos
+ * @return mixed Info demandée ou tableau des infos.
+ */
+ public function getInfos($key = null)
+ {
+ if (is_null($key))
+ {
+ return $this->plugin;
+ }
+
+ if (array_key_exists($key, $this->plugin))
+ {
+ return $this->plugin[$key];
+ }
+
+ return null;
+ }
+
+ /**
+ * Renvoie l'identifiant du plugin
+ * @return string Identifiant du plugin
+ */
+ public function id()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Inclure un fichier depuis le plugin (dynamique ou statique)
+ * @param string $file Chemin du fichier à aller chercher : si c'est un .php il sera inclus,
+ * sinon il sera juste affiché
+ * @return void
+ * @throws UserException Si le fichier n'existe pas ou fait partie des fichiers qui ne peuvent
+ * être appelés que par des méthodes de Plugin.
+ * @throws RuntimeException Si le chemin indiqué tente de sortir du contexte du PHAR
+ */
+ public function call($file)
+ {
+ $file = preg_replace('!^[./]*!', '', $file);
+
+ if (preg_match('!(?:\.\.|[/\\\\]\.|\.[/\\\\])!', $file))
+ {
+ throw new \RuntimeException('Chemin de fichier incorrect.');
+ }
+
+ $forbidden = ['install.php', 'garradin_plugin.ini', 'upgrade.php', 'uninstall.php', 'signals.php'];
+
+ if (in_array($file, $forbidden))
+ {
+ throw new UserException('Le fichier ' . $file . ' ne peut être appelé par cette méthode.');
+ }
+
+ if (!file_exists($this->path() . '/www/' . $file))
+ {
+ throw new UserException('Le fichier ' . $file . ' n\'existe pas dans le plugin ' . $this->id);
+ }
+
+ $plugin = $this;
+ global $tpl, $config, $user, $membres;
+
+ if (substr($file, -4) === '.php')
+ {
+ include $this->path() . '/www/' . $file;
+ }
+ else
+ {
+ // Récupération du type MIME à partir de l'extension
+ $ext = substr($file, strrpos($file, '.')+1);
+
+ if (isset($this->mimes[$ext]))
+ {
+ $mime = $this->mimes[$ext];
+ }
+ else
+ {
+ $mime = 'text/plain';
+ }
+
+ header('Content-Type: ' .$this->mimes[$ext]);
+ header('Content-Length: ' . filesize($this->path() . '/www/' . $file));
+
+ readfile($this->path() . '/www/' . $file);
+ }
+ }
+
+ /**
+ * Désinstaller le plugin
+ * @return boolean TRUE si la suppression a fonctionné
+ */
+ public function uninstall()
+ {
+ if (file_exists($this->path() . '/uninstall.php'))
+ {
+ include $this->path() . '/uninstall.php';
+ }
+
+ unlink(PLUGINS_ROOT . '/' . $this->id . '.tar.gz');
+
+ $db = DB::getInstance();
+ return $db->simpleExec('DELETE FROM plugins WHERE id = ?;', $this->id);
+ }
+
+ /**
+ * Renvoie TRUE si le plugin a besoin d'être mis à jour
+ * (si la version notée dans la DB est différente de la version notée dans garradin_plugin.ini)
+ * @return boolean TRUE si le plugin doit être mis à jour, FALSE sinon
+ */
+ public function needUpgrade()
+ {
+ $infos = parse_ini_file($this->path() . '/garradin_plugin.ini', false);
+
+ if (version_compare($this->plugin['version'], $infos['version'], '!='))
+ return true;
+
+ return false;
+ }
+
+ /**
+ * Mettre à jour le plugin
+ * Appelle le fichier upgrade.php dans l'archive si celui-ci existe.
+ * @return boolean TRUE si tout a fonctionné
+ */
+ public function upgrade()
+ {
+ if (file_exists($this->path() . '/upgrade.php'))
+ {
+ include $this->path() . '/upgrade.php';
+ }
+
+ $db = DB::getInstance();
+ return $db->simpleUpdate('plugins',
+ 'id = \''.$db->escapeString($this->id).'\'',
+ ['version' => $infos['version']]);
+ }
+
+ /**
+ * Liste des plugins installés (en DB)
+ * @return array Liste des plugins triés par nom
+ */
+ static public function listInstalled()
+ {
+ $db = DB::getInstance();
+ $plugins = $db->simpleStatementFetchAssocKey('SELECT id, * FROM plugins ORDER BY nom;');
+ $system = explode(',', PLUGINS_SYSTEM);
+
+ foreach ($plugins as &$row)
+ {
+ $row['system'] = in_array($row['id'], $system);
+ }
+
+ return $plugins;
+ }
+
+ /**
+ * Liste les plugins qui doivent être affichés dans le menu
+ * @return array Tableau associatif id => nom (ou un tableau vide si aucun plugin ne doit être affiché)
+ */
+ static public function listMenu()
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetchAssoc('SELECT id, nom FROM plugins WHERE menu = 1 ORDER BY nom;');
+ }
+
+ /**
+ * Liste les plugins téléchargés mais non installés
+ * @return array Liste des plugins téléchargés
+ */
+ static public function listDownloaded()
+ {
+ $installed = self::listInstalled();
+
+ $list = [];
+ $dir = dir(PLUGINS_ROOT);
+
+ while ($file = $dir->read())
+ {
+ if (substr($file, 0, 1) == '.')
+ continue;
+
+ if (!preg_match('!^([a-z0-9_.-]+)\.tar\.gz$!', $file, $match))
+ continue;
+
+ if (array_key_exists($match[1], $installed))
+ continue;
+
+ $list[$match[1]] = parse_ini_file('phar://' . PLUGINS_ROOT . '/' . $match[1] . '.tar.gz/garradin_plugin.ini', false);
+ }
+
+ $dir->close();
+
+ return $list;
+ }
+
+ /**
+ * Liste des plugins officiels depuis le repository signé
+ * @return array Liste des plugins
+ */
+ static public function listOfficial()
+ {
+ // La liste est stockée en cache une heure pour ne pas tuer le serveur distant
+ if (Static_Cache::expired('plugins_list', 3600 * 24))
+ {
+ $url = parse_url(PLUGINS_URL);
+
+ $context_options = [
+ 'ssl' => [
+ 'verify_peer' => TRUE,
+ // On vérifie en utilisant le certificat maître de CACert
+ 'cafile' => ROOT . '/include/data/cacert.pem',
+ 'verify_depth' => 5,
+ 'CN_match' => $url['host'],
+ 'SNI_enabled' => true,
+ 'SNI_server_name' => $url['host'],
+ 'disable_compression' => true,
+ ]
+ ];
+
+ $context = stream_context_create($context_options);
+
+ try {
+ $result = file_get_contents(PLUGINS_URL, NULL, $context);
+ }
+ catch (\Exception $e)
+ {
+ throw new UserException('Le téléchargement de la liste des plugins a échoué : ' . $e->getMessage());
+ }
+
+ Static_Cache::store('plugins_list', $result);
+ }
+ else
+ {
+ $result = Static_Cache::get('plugins_list');
+ }
+
+ $list = json_decode($result, true);
+ return $list;
+ }
+
+ /**
+ * Vérifier le hash du plugin $id pour voir s'il correspond au hash du fichier téléchargés
+ * @param string $id Identifiant du plugin
+ * @return boolean TRUE si le hash correspond (intégrité OK), sinon FALSE
+ */
+ static public function checkHash($id)
+ {
+ $list = self::fetchOfficialList();
+
+ if (!array_key_exists($id, $list))
+ return null;
+
+ $hash = sha1_file(PLUGINS_ROOT . '/' . $id . '.tar.gz');
+
+ return ($hash === $list[$id]['hash']);
+ }
+
+ /**
+ * Est-ce que le plugin est officiel ?
+ * @param string $id Identifiant du plugin
+ * @return boolean TRUE si le plugin est officiel, FALSE sinon
+ */
+ static public function isOfficial($id)
+ {
+ $list = self::fetchOfficialList();
+ return array_key_exists($id, $list);
+ }
+
+ /**
+ * Télécharge un plugin depuis le repository officiel, et l'installe
+ * @param string $id Identifiant du plugin
+ * @return boolean TRUE si ça marche
+ * @throws LogicException Si le plugin n'est pas dans la liste des plugins officiels
+ * @throws UserException Si le plugin est déjà installé ou que le téléchargement a échoué
+ * @throws RuntimeException Si l'archive téléchargée est corrompue (intégrité du hash ne correspond pas)
+ */
+ static public function download($id)
+ {
+ $list = self::fetchOfficialList();
+
+ if (!array_key_exists($id, $list))
+ {
+ throw new \LogicException($id . ' n\'est pas un plugin officiel (absent de la liste)');
+ }
+
+ if (file_exists(PLUGINS_ROOT . '/' . $id . '.tar.gz'))
+ {
+ throw new UserException('Le plugin '.$id.' existe déjà .');
+ }
+
+ $url = parse_url(PLUGINS_URL);
+
+ $context_options = [
+ 'ssl' => [
+ 'verify_peer' => TRUE,
+ 'cafile' => ROOT . '/include/data/cacert.pem',
+ 'verify_depth' => 5,
+ 'CN_match' => $url['host'],
+ 'SNI_enabled' => true,
+ 'SNI_server_name' => $url['host'],
+ 'disable_compression' => true,
+ ]
+ ];
+
+ $context = stream_context_create($context_options);
+
+ try {
+ copy($list[$id]['phar'], PLUGINS_ROOT . '/' . $id . '.tar.gz', $context);
+ }
+ catch (\Exception $e)
+ {
+ throw new UserException('Le téléchargement du plugin '.$id.' a échoué : ' . $e->getMessage());
+ }
+
+ if (!self::checkHash($id))
+ {
+ unlink(PLUGINS_ROOT . '/' . $id . '.tar.gz');
+ throw new \RuntimeException('L\'archive du plugin '.$id.' est corrompue (le hash SHA1 ne correspond pas).');
+ }
+
+ self::install($id, true);
+
+ return true;
+ }
+
+ /**
+ * Installer un plugin
+ * @param string $id Identifiant du plugin
+ * @param boolean $official TRUE si le plugin est officiel
+ * @return boolean TRUE si tout a fonctionné
+ */
+ static public function install($id, $official = false)
+ {
+ if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz'))
+ {
+ throw new \RuntimeException('Le plugin ' . $id . ' ne semble pas exister et ne peut donc être installé.');
+ }
+
+ if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/garradin_plugin.ini'))
+ {
+ throw new UserException('L\'archive '.$id.'.tar.gz n\'est pas une extension Garradin : fichier garradin_plugin.ini manquant.');
+ }
+
+ $infos = parse_ini_file('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/garradin_plugin.ini', false);
+
+ $required = ['nom', 'description', 'auteur', 'url', 'version', 'menu', 'config'];
+
+ foreach ($required as $key)
+ {
+ if (!array_key_exists($key, $infos))
+ {
+ throw new \RuntimeException('Le fichier garradin_plugin.ini ne contient pas d\'entrée "'.$key.'".');
+ }
+ }
+
+ if (!empty($infos['menu']) && !file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/www/admin/index.php'))
+ {
+ throw new \RuntimeException('Le plugin '.$id.' ne comporte pas de fichier www/admin/index.php alors qu\'il demande à figurer au menu.');
+ }
+
+ $config = '';
+
+ if ((bool)$infos['config'])
+ {
+ if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/config.json'))
+ {
+ throw new \RuntimeException('L\'archive '.$id.'.tar.gz ne comporte pas de fichier config.json
+ alors que le plugin nécessite le stockage d\'une configuration.');
+ }
+
+ if (!file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/www/admin/config.php'))
+ {
+ throw new \RuntimeException('L\'archive '.$id.'.tar.gz ne comporte pas de fichier www/admin/config.php
+ alors que le plugin nécessite le stockage d\'une configuration.');
+ }
+
+ $config = json_decode(file_get_contents('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/config.json'), true);
+
+ if (is_null($config))
+ {
+ throw new \RuntimeException('config.json invalide. Code erreur JSON: ' . json_last_error());
+ }
+
+ $config = json_encode($config);
+ }
+
+ $db = DB::getInstance();
+ $db->simpleInsert('plugins', [
+ 'id' => $id,
+ 'officiel' => (int)(bool)$official,
+ 'nom' => $infos['nom'],
+ 'description'=> $infos['description'],
+ 'auteur' => $infos['auteur'],
+ 'url' => $infos['url'],
+ 'version' => $infos['version'],
+ 'menu' => (int)(bool)$infos['menu'],
+ 'config' => $config,
+ ]);
+
+ if (file_exists('phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/install.php'))
+ {
+ include 'phar://' . PLUGINS_ROOT . '/' . $id . '.tar.gz/install.php';
+ }
+
+ return true;
+ }
+
+ /**
+ * Renvoie la version installée d'un plugin ou FALSE s'il n'est pas installé
+ * @param string $id Identifiant du plugin
+ * @return mixed Numéro de version du plugin ou FALSE
+ */
+ static public function getInstalledVersion($id)
+ {
+ return DB::getInstance()->simpleQuerySingle('SELECT version FROM plugins WHERE id = ?;');
+ }
+}
\ No newline at end of file
diff --git a/include/class.rappels.php b/include/class.rappels.php
new file mode 100644
index 0000000..d970cf6
--- /dev/null
+++ b/include/class.rappels.php
@@ -0,0 +1,208 @@
+simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;', false, (int) $data['id_cotisation']))
+ {
+ throw new UserException('Cotisation inconnue.');
+ }
+
+ $data['id_cotisation'] = (int) $data['id_cotisation'];
+
+ if ((trim($data['delai']) === '') || !is_numeric($data['delai']))
+ {
+ throw new UserException('Délai avant rappel invalide : doit être indiqué en nombre de jours.');
+ }
+
+ $data['delai'] = (int) $data['delai'];
+
+ if (!isset($data['sujet']) || trim($data['sujet']) === '')
+ {
+ throw new UserException('Le sujet du rappel ne peut être vide.');
+ }
+
+ $data['sujet'] = trim($data['sujet']);
+
+ if (!isset($data['texte']) || trim($data['texte']) === '')
+ {
+ throw new UserException('Le contenu du rappel ne peut être vide.');
+ }
+
+ $data['texte'] = trim($data['texte']);
+ }
+
+ /**
+ * Ajouter un rappel
+ * @param array $data Données du rappel
+ * @return integer Numéro ID du rappel créé
+ */
+ public function add($data)
+ {
+ $db = DB::getInstance();
+
+ $this->_checkFields($data);
+
+ $db->simpleInsert('rappels', $data);
+
+ return $db->lastInsertRowId();
+ }
+
+ /**
+ * Modifier un rappel automatique
+ * @param integer $id Numéro du rappel
+ * @param array $data Données du rappel
+ * @return boolean TRUE si tout s'est bien passé
+ * @throws UserException En cas d'erreur dans une donnée à modifier
+ */
+ public function edit($id, $data)
+ {
+ $db = DB::getInstance();
+
+ $this->_checkFields($data);
+
+ return $db->simpleUpdate('rappels', $data, 'id = ' . (int)$id);
+ }
+
+ /**
+ * Supprimer un rappel automatique
+ * @param integer $id Numéro du rappel
+ * @param boolean $delete_history Effacer aussi l'historique des rappels envoyés
+ * @return boolean TRUE en cas de succès
+ */
+ public function delete($id, $delete_history = false)
+ {
+ $db = DB::getInstance();
+
+ $db->exec('BEGIN;');
+
+ if ($delete_history)
+ {
+ $db->simpleExec('DELETE FROM rappels_envoyes WHERE id_rappel = ?;', (int) $id);
+ }
+ else
+ {
+ $db->simpleExec('UPDATE rappels_envoyes SET id_rappel = NULL WHERE id_rappel = ?;', (int) $id);
+ }
+
+ $db->simpleExec('DELETE FROM rappels WHERE id = ?;', (int) $id);
+ $db->exec('END;');
+
+ return true;
+ }
+
+ /**
+ * Renvoie les données sur un rappel
+ * @param integer $id Numéro du rappel
+ * @return array Données du rappel
+ */
+ public function get($id)
+ {
+ return DB::getInstance()->simpleQuerySingle('SELECT * FROM rappels WHERE id = ?;', true, (int)$id);
+ }
+
+ /**
+ * Renvoie le nombre de rappels automatiques enregistrés
+ * @return integer Nombre de rappels
+ */
+ public function countAll()
+ {
+ return DB::getInstance()->simpleQuerySingle('SELECT COUNT(*) FROM rappels;');
+ }
+
+ /**
+ * Liste des rappels triés par cotisation
+ * @return array Liste des rappels
+ */
+ public function listByCotisation()
+ {
+ return DB::getInstance()->simpleStatementFetch('SELECT r.*,
+ c.intitule, c.montant, c.duree, c.debut, c.fin
+ FROM rappels AS r
+ INNER JOIN cotisations AS c ON c.id = r.id_cotisation
+ ORDER BY r.id_cotisation, r.delai, r.sujet;');
+ }
+
+ /**
+ * Liste des rappels pour une cotisation donnée
+ * @param integer $id Numéro du rappel
+ * @return array Liste des rappels
+ */
+ public function listForCotisation($id)
+ {
+ return DB::getInstance()->simpleStatementFetch('SELECT * FROM rappels
+ WHERE id_cotisation = ? ORDER BY delai, sujet;', \SQLITE3_ASSOC, (int)$id);
+ }
+
+ /**
+ * Envoi des rappels automatiques par e-mail
+ * @return boolean TRUE en cas de succès
+ */
+ public function sendPending()
+ {
+ $db = DB::getInstance();
+ $config = Config::getInstance();
+
+ // Requête compliquée qui fait tout le boulot
+ // la logique est un JOIN des tables rappels, cotisations, cotisations_membres et membres
+ // pour récupérer la liste des membres qui doivent recevoir une cotisation
+ $query = '
+ SELECT
+ *,
+ /* Nombre de jours avant ou après expiration */
+ (julianday(date()) - julianday(expiration)) AS nb_jours,
+ /* Date de mise en Åuvre du rappel */
+ date(expiration, delai || \' days\') AS date_rappel
+ FROM (
+ SELECT m.*, r.delai, r.sujet, r.texte, r.id_cotisation,
+ m.'.$config->get('champ_identite').' AS identite,
+ CASE WHEN c.duree IS NOT NULL THEN date(cm.date, \'+\'||c.duree||\' days\')
+ WHEN c.fin IS NOT NULL THEN c.fin ELSE 0 END AS expiration
+ FROM rappels AS r
+ INNER JOIN cotisations AS c ON c.id = r.id_cotisation
+ INNER JOIN cotisations_membres AS cm ON cm.id_cotisation = c.id
+ INNER JOIN membres AS m ON m.id = cm.id_membre
+ WHERE
+ /* Inutile de sélectionner les membres sans email */
+ m.email IS NOT NULL AND m.email != \'\'
+ /* Les cotisations ponctuelles ne comptent pas */
+ AND (c.fin IS NOT NULL OR c.duree IS NOT NULL)
+ /* Rien nest envoyé aux membres des catégories cachées, logique */
+ AND m.id_categorie NOT IN (SELECT id FROM membres_categories WHERE cacher = 1)
+ ORDER BY r.delai ASC
+ )
+ WHERE nb_jours >= delai
+ /* Pour ne pas spammer on n\'envoie pas de rappel antérieur au dernier rappel déjà effectué */
+ AND id NOT IN (SELECT id_membre FROM rappels_envoyes AS re
+ WHERE id_cotisation = re.id_cotisation AND id = re.id_membre
+ AND re.date >= date(expiration, delai || \' days\')
+ )
+ /* Grouper par membre, pour n\'envoyer qu\'un seul rappel par membre/cotise */
+ GROUP BY id, id_cotisation
+ ORDER BY nb_jours DESC;';
+
+ $db->exec('BEGIN');
+ $st = $db->prepare($query);
+ $res = $st->execute();
+ $re = new Rappels_Envoyes;
+
+ while ($row = $res->fetchArray(DB::ASSOC))
+ {
+ $re->sendAuto($row);
+ }
+
+ $db->exec('END;');
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/include/class.rappels_envoyes.php b/include/class.rappels_envoyes.php
new file mode 100644
index 0000000..4bb67cd
--- /dev/null
+++ b/include/class.rappels_envoyes.php
@@ -0,0 +1,231 @@
+simpleQuerySingle('SELECT 1 FROM cotisations WHERE id = ?;', false, (int) $data['id_cotisation']))
+ {
+ throw new UserException('Cotisation inconnue.');
+ }
+
+ $data['id_cotisation'] = (int) $data['id_cotisation'];
+ }
+
+ if (empty($data['id_membre'])
+ || !$db->simpleQuerySingle('SELECT 1 FROM membres WHERE id = ?;', false, (int) $data['id_membre']))
+ {
+ throw new UserException('Membre inconnu.');
+ }
+
+ $data['id_membre'] = (int) $data['id_membre'];
+
+ if (empty($data['media']) || !is_numeric($data['media'])
+ || !in_array((int)$data['media'], [self::MEDIA_EMAIL, self::MEDIA_COURRIER, self::MEDIA_TELEPHONE, self::MEDIA_AUTRE]))
+ {
+ throw new UserException('Média invalide.');
+ }
+
+ $data['media'] = (int) $data['media'];
+
+ if (empty($data['date']) || !utils::checkDate($data['date']))
+ {
+ throw new UserException('La date indiquée n\'est pas valide.');
+ }
+ }
+
+ /**
+ * Enregistrer un rappel
+ * @param array $data Données du rappel
+ * @return integer Numéro ID du rappel créé
+ */
+ public function add($data)
+ {
+ $db = DB::getInstance();
+
+ $this->_checkFields($data);
+
+ $db->simpleInsert('rappels_envoyes', $data);
+
+ return $db->lastInsertRowId();
+ }
+
+ /**
+ * Supprimer un rappel enregistré
+ * @param integer $id Numéro du rappel
+ * @return boolean TRUE en cas de succès
+ */
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+ $db->simpleExec('DELETE FROM rappels_envoyes WHERE id = ?;', (int) $id);
+ return true;
+ }
+
+ /**
+ * Renvoie les données sur un rappel
+ * @param integer $id Numéro du rappel
+ * @return array Données du rappel
+ */
+ public function get($id)
+ {
+ return DB::getInstance()->simpleQuerySingle('SELECT * FROM rappels_envoyes WHERE id = ?;', true, (int)$id);
+ }
+
+ /**
+ * Remplacer les tags dans le contenu/sujet du mail
+ * @param string $content Chaîne à traiter
+ * @param array $data Données supplémentaires à utiliser comme tags (tableau associatif)
+ * @return string $content dont les tags ont été remplacés par le contenu correct
+ */
+ public function replaceTagsInContent($content, $data = null)
+ {
+ $config = Config::getInstance();
+ $tags = [
+ '#NOM_ASSO' => $config->get('nom_asso'),
+ '#ADRESSE_ASSO' => $config->get('adresse_asso'),
+ '#EMAIL_ASSO' => $config->get('email_asso'),
+ '#SITE_ASSO' => $config->get('site_asso'),
+ '#URL_RACINE' => WWW_URL,
+ '#URL_SITE' => WWW_URL,
+ '#URL_ADMIN' => WWW_URL . 'admin/',
+ ];
+
+ if (!empty($data) && is_array($data))
+ {
+ foreach ($data as $key=>$value)
+ {
+ $key = '#' . strtoupper($key);
+ $tags[$key] = $value;
+ }
+ }
+
+ return strtr($content, $tags);
+ }
+
+ /**
+ * Envoi de mail pour rappel automatisé
+ * @param array $data Données du rappel automatisé
+ * @return boolean TRUE
+ */
+ public function sendAuto($data)
+ {
+ $replace = $data;
+ $replace['date_rappel'] = utils::sqliteDateToFrench($replace['date_rappel']);
+ $replace['date_expiration'] = utils::sqliteDateToFrench($replace['expiration']);
+ $replace['nb_jours'] = abs($replace['nb_jours']);
+ $replace['delai'] = abs($replace['delai']);
+
+ $subject = $this->replaceTagsInContent($data['sujet'], $replace);
+ $text = $this->replaceTagsInContent($data['texte'], $replace);
+
+ // Envoi du mail
+ utils::mail($data['email'], $subject, $text);
+
+ // Enregistrement en DB
+ $this->add([
+ 'id_cotisation' => $data['id_cotisation'],
+ 'id_membre' => $data['id'],
+ 'media' => Rappels_Envoyes::MEDIA_EMAIL,
+ // On enregistre la date de mise en Åuvre du rappel
+ // et non pas la date d'envoi effective du rappel
+ // car l'envoi du rappel peut ne pas être effectué
+ // le jour où il aurait dû être envoyé (la magie des cron)
+ 'date' => $data['date_rappel'],
+ ]);
+
+ return true;
+ }
+
+ /**
+ * Liste des rappels envoyés à un membre
+ * @param integer $id Numéro du membre
+ * @return array Liste des rappels
+ */
+ public function listForMember($id)
+ {
+ return DB::getInstance()->simpleStatementFetch('SELECT
+ re.*, c.intitule, c.montant
+ FROM rappels_envoyes AS re
+ INNER JOIN cotisations AS c ON c.id = re.id_cotisation
+ WHERE re.id_membre = ?
+ ORDER BY re.date DESC;', \SQLITE3_ASSOC, (int)$id);
+ }
+
+ /**
+ * Liste des rappels pour une cotisation donnée
+ * @param integer $id Numéro de la cotisation
+ * @param integer $page Numéro de page de liste
+ * @return array Liste des rappels
+ */
+ public function listForCotisation($id, $page = 1)
+ {
+ $begin = ($page - 1) * self::ITEMS_PER_PAGE;
+
+ return DB::getInstance()->simpleStatementFetch('SELECT * FROM rappels_envoyes
+ WHERE id_rappel IN (SELECT id FROM rappels WHERE id_cotisation = ?)
+ ORDER BY date DESC;', \SQLITE3_ASSOC, (int)$id);
+ }
+
+ /**
+ * Nombre de rappels pour une cotisation donnée
+ * @param integer $id Numéro de la cotisation
+ * @return integer Nombre de rappels envoyés
+ */
+ public function countForCotisation($id)
+ {
+ return DB::getInstance()->simpleQuerySingle('SELECT COUNT(*) FROM rappels_envoyes
+ WHERE id_rappel IN (SELECT id FROM rappels WHERE id_cotisation = ?);',
+ false, (int)$id);
+ }
+
+ /**
+ * Liste des rappels envoyés pour un rappel automatique
+ * @param integer $id Numéro du rappel
+ * @param integer $page Numéro de page de liste
+ * @return array Liste des rappels envoyés
+ */
+ public function listForRappel($id, $page = 1)
+ {
+ $begin = ($page - 1) * self::ITEMS_PER_PAGE;
+
+ return DB::getInstance()->simpleStatementFetch('SELECT * FROM rappels_envoyes
+ WHERE id_rappel = ? ORDER BY date DESC LIMIT ?,?;',
+ \SQLITE3_ASSOC, (int)$id, (int)$begin, self::ITEMS_PER_PAGE);
+ }
+
+ /**
+ * Nombre de rappels envoyés pour un rappel automatique
+ * @param integer $id Numéro du rappel
+ * @return integer Nombre de rappels envoyés pour ce rappel
+ */
+ public function countForRappel($id)
+ {
+ return DB::getInstance()->simpleQuerySingle('SELECT COUNT(*) FROM rappels_envoyes
+ WHERE id_rappel = ?;', false, (int)$id);
+ }
+}
\ No newline at end of file
diff --git a/include/class.sauvegarde.php b/include/class.sauvegarde.php
new file mode 100644
index 0000000..4fae1cb
--- /dev/null
+++ b/include/class.sauvegarde.php
@@ -0,0 +1,263 @@
+read())
+ {
+ if ($file[0] != '.' && is_file(DATA_ROOT . '/' . $file)
+ && preg_match('![\w\d._-]+\.' . $ext . '$!i', $file) && $file != basename(DB_FILE))
+ {
+ $out[$file] = filemtime(DATA_ROOT . '/' . $file);
+ }
+ }
+
+ $dir->close();
+
+ ksort($out);
+
+ return $out;
+ }
+
+ /**
+ * Crée une nouvelle sauvegarde
+ * @param boolean $auto Si true le nom de fichier sera celui de la sauvegarde automatique courante,
+ * sinon le nom sera basé sur la date (sauvegarde manuelle)
+ * @return string Le nom de fichier de la sauvegarde ainsi créée
+ */
+ public function create($auto = false)
+ {
+ $backup = str_replace('.sqlite', ($auto ? '.auto.1' : date('.Y-m-d-His')) . '.sqlite', DB_FILE);
+ copy(DB_FILE, $backup);
+ return basename($backup);
+ }
+
+ /**
+ * Effectue une rotation des sauvegardes automatiques
+ * association.auto.1.sqlite deviendra association.auto.2.sqlite par exemple
+ * @return boolean true
+ */
+ public function rotate()
+ {
+ $config = Config::getInstance();
+ $nb = $config->get('nombre_sauvegardes');
+
+ $list = $this->getList(true);
+ krsort($list);
+
+ if (count($list) >= $nb)
+ {
+ $this->remove(key($list));
+ array_shift($list);
+ }
+
+ foreach ($list as $f=>$d)
+ {
+ $new = preg_replace_callback('!\.auto\.(\d+)\.sqlite$!', function ($m) {
+ return '.auto.' . ((int) $m[1] + 1) . '.sqlite';
+ }, $f);
+
+ rename(DATA_ROOT . '/' . $f, DATA_ROOT . '/' . $new);
+ }
+
+ return true;
+ }
+
+ /**
+ * Crée une sauvegarde automatique si besoin est
+ * @return boolean true
+ */
+ public function auto()
+ {
+ $config = Config::getInstance();
+
+ // Pas besoin d'aller plus loin si on ne fait pas de sauvegarde auto
+ if ($config->get('frequence_sauvegardes') == 0 || $config->get('nombre_sauvegardes') == 0)
+ return true;
+
+ $list = $this->getList(true);
+
+ if (count($list) > 0)
+ {
+ $last = current($list);
+ }
+ else
+ {
+ $last = false;
+ }
+
+ // Test de la date de création de la dernière sauvegarde
+ if ($last >= (time() - ($config->get('frequence_sauvegardes') * 3600 * 24)))
+ {
+ return true;
+ }
+
+ // Si pas de modif depuis la dernière sauvegarde, ça sert à rien d'en faire
+ if ($last >= filemtime(DB_FILE))
+ {
+ return true;
+ }
+
+ $this->rotate();
+ $this->create(true);
+
+ return true;
+ }
+
+ /**
+ * Efface une sauvegarde locale
+ * @param string $file Nom du fichier à supprimer
+ * @return boolean true si le fichier a bien été supprimé, false sinon
+ */
+ public function remove($file)
+ {
+ if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._-]+\.sqlite$!i', $file)
+ || $file == basename(DB_FILE))
+ {
+ throw new UserException('Nom de fichier non valide.');
+ }
+
+ return unlink(DATA_ROOT . '/' . $file);
+ }
+
+ /**
+ * Renvoie sur la sortie courante le contenu du fichier de base de données courant
+ * @return boolean true
+ */
+ public function dump()
+ {
+ $in = fopen(DB_FILE, 'r');
+ $out = fopen('php://output', 'w');
+
+ while (!feof($in))
+ {
+ fwrite($out, fread($in, 8192));
+ }
+
+ fclose($in);
+ fclose($out);
+ return true;
+ }
+
+ /**
+ * Restaure une sauvegarde locale
+ * @param string $file Le nom de fichier à utiliser comme point de restauration
+ * @return boolean true si la restauration a fonctionné, false sinon
+ */
+ public function restoreFromLocal($file)
+ {
+ if (preg_match('!\.\.+!', $file) || !preg_match('!^[\w\d._-]+$!i', $file))
+ {
+ throw new UserException('Nom de fichier non valide.');
+ }
+
+ if (!file_exists(DATA_ROOT . '/' . $file))
+ {
+ throw new UserException('Le fichier fourni n\'existe pas.');
+ }
+
+ return $this->restoreDB(DATA_ROOT . '/' . $file);
+ }
+
+ /**
+ * Restaure une copie distante (fichier envoyé)
+ * @param array $file Tableau provenant de $_FILES
+ * @return boolean true
+ */
+ public function restoreFromUpload($file)
+ {
+ if (empty($file['size']) || empty($file['tmp_name']) || !empty($file['error']))
+ {
+ throw new UserException('Le fichier n\'a pas été correctement envoyé. Essayer de le renvoyer à nouveau.');
+ }
+
+ $r = $this->restoreDB($file['tmp_name']);
+
+ if ($r)
+ {
+ unlink($file['tmp_name']);
+ }
+
+ return $r;
+ }
+
+ /**
+ * Restauration de base de données, la fonction qui le fait vraiment
+ * @param string $file Chemin absolu vers la base de données à utiliser
+ * @return mixed true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB
+ * ne correspond pas à la version de Garradin (mise à jour nécessaire).
+ */
+ protected function restoreDB($file)
+ {
+ // Essayons déjà d'ouvrir la base de données à restaurer en lecture
+ try {
+ $db = new \SQLite3($file, SQLITE3_OPEN_READONLY);
+ }
+ catch (\Exception $e)
+ {
+ throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
+ 'Message d\'erreur de SQLite : ' . $e->getMessage());
+ }
+
+ // Regardons ensuite si la base de données n'est pas corrompue
+ $check = $db->querySingle('PRAGMA integrity_check;');
+
+ if (strtolower(trim($check)) != 'ok')
+ {
+ throw new UserException('Le fichier fourni est corrompu. SQLite a trouvé ' . $check . ' erreurs.');
+ }
+
+ // On ne peut pas faire de vérifications très poussées sur la structure de la base de données,
+ // celle-ci pouvant changer d'une version à l'autre et on peut vouloir importer une base
+ // un peu vieille, mais on vérifie quand même que ça ressemble un minimum à une base garradin
+ $table = $db->querySingle('SELECT 1 FROM sqlite_master WHERE type=\'table\' AND tbl_name=\'config\';');
+
+ if (!$table)
+ {
+ throw new UserException('Le fichier fourni ne semble pas contenir de données liées à Garradin.');
+ }
+
+ // On récupère la version pour plus tard
+ $version = $db->querySingle('SELECT valeur FROM config WHERE cle=\'version\';');
+
+ $db->close();
+
+ $backup = str_replace('.sqlite', date('.Y-m-d-His') . '.avant_restauration.sqlite', DB_FILE);
+
+ if (!rename(DB_FILE, $backup))
+ {
+ throw new \RuntimeException('Unable to backup current DB file.');
+ }
+
+ if (!copy($file, DB_FILE))
+ {
+ rename($backup, DB_FILE);
+ throw new \RuntimeException('Unable to copy backup DB to main location.');
+ }
+
+ if ($version != garradin_version())
+ {
+ return self::NEED_UPGRADE;
+ }
+
+ return true;
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.squelette.php b/include/class.squelette.php
new file mode 100644
index 0000000..d659981
--- /dev/null
+++ b/include/class.squelette.php
@@ -0,0 +1,753 @@
+_getType($type, $value);
+
+ if ($type == self::OBJ)
+ {
+ $this->_content = $value->get();
+ }
+ else
+ {
+ $this->_content[] = (string) (int) $type . $value;
+ }
+
+ unset($value);
+ }
+
+ public function prepend($type = self::TEXT, $value, $pos = false)
+ {
+ $type = $this->_getType($type, $value);
+
+ if ($type == self::OBJ)
+ {
+ if ($pos)
+ {
+ array_splice($this->_content, $pos, 0, $value->get());
+ }
+ else
+ {
+ $this->_content = array_merge($value->get(), $this->_content);
+ }
+ }
+ else
+ {
+ $value = (string) (int) $type . $value;
+
+ if ($pos)
+ {
+ array_splice($this->_content, $pos, 0, $value);
+ }
+ else
+ {
+ array_unshift($this->_content, $value);
+ }
+ }
+
+ unset($value);
+ }
+
+ public function append($type = self::TEXT, $value, $pos = false)
+ {
+ $type = $this->_getType($type, $value);
+
+ if ($type == self::OBJ)
+ {
+ if ($pos)
+ {
+ array_splice($this->_content, $pos + 1, 0, $value->get());
+ }
+ else
+ {
+ $this->_content = array_merge($this->_content, $value->get());
+ }
+ }
+ else
+ {
+ $value = (string) (int) $type . $value;
+
+ if ($pos)
+ {
+ array_splice($this->_content, $pos + 1, 0, $value);
+ }
+ else
+ {
+ array_push($this->_content, $value);
+ }
+ }
+
+ unset($value);
+ }
+
+ public function output($in_php = false)
+ {
+ $out = '';
+ $php = $in_php ?: false;
+
+ foreach ($this->_content as $line)
+ {
+ if ($line[0] == self::PHP && !$php)
+ {
+ $php = true;
+ $out .= '';
+ }
+
+ $out .= substr($line, 1);
+
+ if ($line[0] == self::PHP)
+ {
+ $out .= "\n";
+ }
+ }
+
+ if ($php && !$in_php)
+ {
+ $out .= ' ?>';
+ }
+
+ $this->_content = [];
+
+ return $out;
+ }
+
+ public function __toString()
+ {
+ return $this->output(false);
+ }
+
+ public function get()
+ {
+ return $this->_content;
+ }
+
+ public function replace($key, $type = self::TEXT, $value)
+ {
+ $type = $this->_getType($type, $value);
+
+ if ($type == self::OBJ)
+ {
+ array_splice($this->_content, $key, 1, $value->get());
+ }
+ else
+ {
+ $this->_content[$key] = (string) (int) $type . $value;
+ }
+
+ unset($value);
+ }
+}
+
+class Squelette extends \miniSkel
+{
+ private $parent = null;
+ private $current = null;
+ private $_vars = [];
+
+ private function _registerDefaultModifiers()
+ {
+ foreach (Squelette_Filtres::$filtres_php as $func=>$name)
+ {
+ if (is_string($func))
+ $this->register_modifier($name, $func);
+ else
+ $this->register_modifier($name, $name);
+ }
+
+ foreach (get_class_methods('Garradin\Squelette_Filtres') as $name)
+ {
+ $this->register_modifier($name, ['Garradin\Squelette_Filtres', $name]);
+ }
+
+ foreach (Squelette_Filtres::$filtres_alias as $name=>$func)
+ {
+ $this->register_modifier($name, ['Garradin\Squelette_Filtres', $func]);
+ }
+ }
+
+ public function __construct()
+ {
+ $this->_registerDefaultModifiers();
+
+ $config = Config::getInstance();
+
+ $this->assign('nom_asso', $config->get('nom_asso'));
+ $this->assign('adresse_asso', $config->get('adresse_asso'));
+ $this->assign('email_asso', $config->get('email_asso'));
+ $this->assign('site_asso', $config->get('site_asso'));
+
+ $this->assign('url_racine', WWW_URL);
+ $this->assign('url_site', WWW_URL);
+ $this->assign('url_atom', WWW_URL . 'feed/atom/');
+ $this->assign('url_elements', WWW_URL . 'squelettes/');
+ $this->assign('url_admin', WWW_URL . 'admin/');
+ }
+
+ protected function processInclude($args)
+ {
+ if (empty($args))
+ throw new \miniSkelMarkupException("Le tag INCLURE demande à préciser le fichier à inclure.");
+
+ $file = key($args);
+
+ if (empty($file) || !preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $file))
+ throw new \miniSkelMarkupException("INCLURE: le nom de fichier ne peut contenir que des caractères alphanumériques.");
+
+ return new Squelette_Snippet(1, '$this->fetch("'.$file.'", false);');
+ }
+
+ protected function processVariable($name, $value, $applyDefault, $modifiers, $pre, $post, $context)
+ {
+ if ($context == self::CONTEXT_IN_ARG)
+ {
+ $out = new Squelette_Snippet(1, '$this->getVariable(\''.$name.'\')');
+
+ if ($pre)
+ {
+ $out->prepend(2, $pre);
+ }
+
+ if ($post)
+ {
+ $out->append(2, $post);
+ }
+
+ return $out;
+ }
+
+ $out = new Squelette_Snippet(1, '$value = $this->getVariable(\''.$name.'\');');
+
+ // We process modifiers
+ foreach ($modifiers as &$modifier)
+ {
+ if (!isset($this->modifiers[$modifier['name']]))
+ {
+ throw new \miniSkelMarkupException('Filtre '.$modifier['name'].' inconnu !');
+ }
+
+ $out->append(1, '$value = call_user_func_array('.var_export($this->modifiers[$modifier['name']], true).', [$value, ');
+
+ foreach ($modifier['arguments'] as $arg)
+ {
+ if ($arg == 'debut_liste')
+ {
+ $out->append(1, '$this->getVariable(\'debut_liste\')');
+ }
+ elseif ($arg instanceOf Squelette_Snippet)
+ {
+ $out->append(3, $arg);
+ }
+ else
+ {
+ //if (preg_match('!getVariable!', $arg)) throw new Exception("lol");
+ $out->append(1, '"'.str_replace('"', '\\"', $arg).'"');
+ }
+
+ $out->append(1, ', ');
+ }
+
+ $out->append(1, ']);');
+
+ if (in_array($modifier['name'], Squelette_Filtres::$desactiver_defaut))
+ {
+ $applyDefault = false;
+ }
+ }
+
+ if ($applyDefault)
+ {
+ $out->append(1, 'if (is_string($value) && trim($value)) $value = htmlspecialchars($value, ENT_QUOTES, \'UTF-8\', false);');
+ }
+
+ $out->append(1, 'if ($value === true || trim($value) !== \'\'):');
+
+ // Getting pre-content
+ if ($pre)
+ {
+ $out->append(2, $pre);
+ }
+
+ $out->append(1, 'echo is_bool($value) ? "" : $value;');
+
+ // Getting post-content
+ if ($post)
+ {
+ $out->append(2, $post);
+ }
+
+ $out->append(1, 'endif;');
+
+ return $out;
+ }
+
+ protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent)
+ {
+ if ($loopType != 'articles' && $loopType != 'rubriques' && $loopType != 'pages')
+ {
+ throw new \miniSkelMarkupException("Le type de boucle '".$loopType."' est inconnu.");
+ }
+
+ $loopStart = '';
+ $query = $where = $order = '';
+ $limit = $begin = 0;
+
+ $query = 'SELECT w.*, strftime(\\\'%s\\\', w.date_creation) AS date_creation, strftime(\\\'%s\\\', w.date_modification) AS date_modification';
+
+ if (trim($loopContent))
+ {
+ $query .= ', r.contenu AS texte FROM wiki_pages AS w LEFT JOIN wiki_revisions AS r ON (w.id = r.id_page AND w.revision = r.revision) ';
+ }
+ else
+ {
+ $query .= '\'\' AS texte ';
+ }
+
+ $where = 'WHERE w.droit_lecture = -1 ';
+
+ if ($loopType == 'articles')
+ {
+ $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id) = 0 ';
+ }
+ elseif ($loopType == 'rubriques')
+ {
+ $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id) > 0 ';
+ }
+
+ $allowed_fields = ['id', 'uri', 'titre', 'date', 'date_creation', 'date_modification',
+ 'parent', 'rubrique', 'revision', 'points', 'recherche', 'texte'];
+ $search = $search_rank = false;
+
+ foreach ($loopCriterias as $criteria)
+ {
+ if (isset($criteria['field']))
+ {
+ if (!in_array($criteria['field'], $allowed_fields))
+ {
+ throw new \miniSkelMarkupException("Critère '".$criteria['field']."' invalide pour la boucle '$loopName' de type '$loopType'.");
+ }
+ elseif ($criteria['field'] == 'rubrique')
+ {
+ $criteria['field'] = 'parent';
+ }
+ elseif ($criteria['field'] == 'date')
+ {
+ $criteria['field'] = 'date_creation';
+ }
+ elseif ($criteria['field'] == 'points')
+ {
+ if ($criteria['action'] != \miniSkel::ACTION_ORDER_BY)
+ {
+ throw new \miniSkelMarkupException("Le critère 'points' n\'est pas valide dans ce contexte.");
+ }
+
+ $search_rank = true;
+ }
+ }
+
+ switch ($criteria['action'])
+ {
+ case \miniSkel::ACTION_ORDER_BY:
+ if (!$order)
+ $order = 'ORDER BY '.$criteria['field'].'';
+ else
+ $order .= ', '.$criteria['field'].'';
+ break;
+ case \miniSkel::ACTION_ORDER_DESC:
+ if ($order)
+ $order .= ' DESC';
+ break;
+ case \miniSkel::ACTION_LIMIT:
+ $begin = $criteria['begin'];
+ $limit = $criteria['number'];
+ break;
+ case \miniSkel::ACTION_MATCH_FIELD_BY_VALUE:
+ $where .= ' AND '.$criteria['field'].' '.$criteria['comparison'].' \\\'\'.$db->escapeString(\''.$criteria['value'].'\').\'\\\'';
+ break;
+ case \miniSkel::ACTION_MATCH_FIELD:
+ {
+ if ($criteria['field'] == 'recherche')
+ {
+ $query = 'SELECT w.*, r.contenu AS texte, rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points FROM wiki_pages AS w INNER JOIN wiki_recherche AS r ON (w.id = r.id) ';
+ $where .= ' AND wiki_recherche MATCH \\\'\'.$db->escapeString($this->getVariable(\''.$criteria['field'].'\')).\'\\\'';
+ $search = true;
+ }
+ else
+ {
+ if ($criteria['field'] == 'parent')
+ $field = 'id';
+ else
+ $field = $criteria['field'];
+
+ $where .= ' AND '.$criteria['field'].' = \\\'\'.$db->escapeString($this->getVariable(\''.$field.'\')).\'\\\'';
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ if ($search_rank && !$search)
+ {
+ throw new \miniSkelMarkupException("Le critère par points n'est possible que dans les boucles de recherche.");
+ }
+
+ if (trim($loopContent))
+ {
+ $loopStart .= '$row[\'url\'] = WWW_URL . $row[\'uri\']; ';
+ }
+
+ $query .= $where . ' ' . $order;
+
+ if (!$limit || $limit > 100)
+ $limit = 100;
+
+ if ($limit)
+ {
+ $query .= ' LIMIT '.(is_numeric($begin) ? (int) $begin : '\'.$this->variables[\'debut_liste\'].\'').','.(int)$limit;
+ }
+
+ $hash = sha1(uniqid(mt_rand(), true));
+ $out = new Squelette_Snippet();
+ $out->append(1, '$parent_hash = $this->current[\'_self_hash\'];');
+ $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$parent_hash] : null;');
+
+ if ($search)
+ {
+ $out->append(1, 'if (trim($this->getVariable(\'recherche\'))) { ');
+ }
+
+ $out->append(1, '$statement = $db->prepare(\''.$query.'\'); ');
+ // Sécurité anti injection
+ $out->append(1, 'if (!$statement->readOnly()) { throw new \\miniSkelMarkupException("Requête en écriture illégale: '.$query.'"); } ');
+ $out->append(1, '$result_'.$hash.' = $statement->execute(); ');
+ $out->append(1, '$nb_rows = $db->countRows($result_'.$hash.'); ');
+
+ if ($search)
+ {
+ $out->append(1, '} else { $result_'.$hash.' = false; $nb_rows = 0; }');
+ }
+
+ $out->append(1, '$this->_vars[\''.$hash.'\'] = [\'_self_hash\' => \''.$hash.'\', \'_parent_hash\' => $parent_hash, \'total_boucle\' => $nb_rows, \'compteur_boucle\' => 0];');
+ $out->append(1, '$this->current =& $this->_vars[\''.$hash.'\']; ');
+ $out->append(1, 'if ($nb_rows > 0):');
+
+ if ($preContent)
+ {
+ $out->append(2, $this->parse($preContent, $loopName, self::PRE_CONTENT));
+ }
+
+ $out->append(1, 'while ($row = $result_'.$hash.'->fetchArray(SQLITE3_ASSOC)): ');
+ $out->append(1, '$this->_vars[\''.$hash.'\'][\'compteur_boucle\'] += 1; ');
+ $out->append(1, $loopStart);
+ $out->append(1, '$this->_vars[\''.$hash.'\'] = array_merge($this->_vars[\''.$hash.'\'], $row); ');
+
+ $out->append(2, $this->parseVariables($loopContent));
+
+ $out->append(1, 'endwhile;');
+
+ // we put the post-content after the loop content
+ if ($postContent)
+ {
+ $out->append(2, $this->parse($postContent, $loopName, self::POST_CONTENT));
+ }
+
+ if ($altContent)
+ {
+ $out->append(1, 'else:');
+ $out->append(2, $this->parse($altContent, $loopName, self::ALT_CONTENT));
+ }
+
+ $out->append(1, 'endif; ');
+ $out->append(1, '$parent_hash = $this->_vars[\''.$hash.'\'][\'_parent_hash\']; ');
+ $out->append(1, 'unset($result_'.$hash.', $nb_rows, $this->_vars[\''.$hash.'\']); ');
+ $out->append(1, 'if ($parent_hash) { $this->current =& $this->_vars[$parent_hash]; $parent_hash = $this->current[\'_parent_hash\']; } ');
+ $out->append(1, 'else { $this->current = null; }');
+ $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$_parent_hash] : null;');
+
+ return $out;
+ }
+
+ public function fetch($template, $no_display = false)
+ {
+ $this->currentTemplate = $template;
+
+ $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template)
+ ? DATA_ROOT . '/www/squelettes/' . $template
+ : ROOT . '/www/squelettes-dist/' . $template;
+
+ $tpl_id = basename(dirname($path)) . '/' . $template;
+
+ if (!self::compile_check($tpl_id, $path))
+ {
+ if (!file_exists($path))
+ {
+ throw new \miniSkelMarkupException('Le squelette "'.$tpl_id.'" n\'existe pas.');
+ }
+
+ $content = file_get_contents($path);
+ $content = strtr($content, [' '<?php', '' => '']);
+
+ $out = new Squelette_Snippet(2, $this->parse($content));
+ $out->prepend(1, '/* '.$tpl_id.' */ '.
+ 'namespace Garradin; $db = DB::getInstance(); '.
+ 'if ($this->parent) $parent_hash = $this->parent[\'_self_hash\']; '. // For included files
+ 'else $parent_hash = false;');
+
+ if (!$no_display)
+ {
+ self::compile_store($tpl_id, $out);
+ }
+ }
+
+ if (!$no_display)
+ {
+ require self::compile_get_path($tpl_id);
+ }
+ else
+ {
+ eval($tpl_id);
+ }
+
+ return null;
+ }
+
+ public function dispatchURI()
+ {
+ $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
+
+ header('HTTP/1.1 200 OK', 200, true);
+
+ if ($pos = strpos($uri, '?'))
+ {
+ $uri = substr($uri, 0, $pos);
+ }
+ else
+ {
+ // WWW_URI inclus toujours le slash final, mais on veut le conserver ici
+ $uri = substr($uri, strlen(WWW_URI) - 1);
+ }
+
+ if ($uri == '/')
+ {
+ $skel = 'sommaire.html';
+ }
+ elseif ($uri == '/feed/atom/')
+ {
+ header('Content-Type: application/atom+xml');
+ $skel = 'atom.xml';
+ }
+ elseif (substr($uri, -1) == '/')
+ {
+ $skel = 'rubrique.html';
+ $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1, -1);
+ }
+ elseif (preg_match('!^/admin/!', $uri))
+ {
+ throw new UserException('Cette page n\'existe pas.');
+ }
+ else
+ {
+ $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1);
+
+ if (preg_match('!^[\w\d_-]+$!i', $_GET['uri'])
+ && file_exists(DATA_ROOT . '/www/squelettes/' . strtolower($_GET['uri']) . '.html'))
+ {
+ $skel = strtolower($_GET['uri']) . '.html';
+ }
+ else
+ {
+ $skel = 'article.html';
+ }
+ }
+
+ $this->display($skel);
+ }
+
+ static private function compile_get_path($path)
+ {
+ $hash = sha1($path);
+ return DATA_ROOT . '/cache/compiled/s_' . $hash . '.php';
+ }
+
+ static private function compile_check($tpl, $check)
+ {
+ if (!file_exists(self::compile_get_path($tpl)))
+ return false;
+
+ $time = filemtime(self::compile_get_path($tpl));
+
+ if (empty($time))
+ {
+ return false;
+ }
+
+ if ($time < filemtime($check))
+ return false;
+ return $time;
+ }
+
+ static private function compile_store($tpl, $content)
+ {
+ $path = self::compile_get_path($tpl);
+
+ if (!file_exists(dirname($path)))
+ {
+ mkdir(dirname($path));
+ }
+
+ file_put_contents($path, $content);
+ return true;
+ }
+
+ static public function compile_clear($tpl)
+ {
+ $path = self::compile_get_path($tpl);
+
+ if (file_exists($path))
+ unlink($path);
+
+ return true;
+ }
+
+ protected function getVariable($var)
+ {
+ if (isset($this->current[$var]))
+ {
+ return $this->current[$var];
+ }
+ elseif (isset($this->parent[$var]))
+ {
+ return $this->parent[$var];
+ }
+ elseif (isset($this->variables[$var]))
+ {
+ return $this->variables[$var];
+ }
+ elseif (isset($_REQUEST[$var]))
+ {
+ return $_REQUEST[$var];
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ static public function getSource($template)
+ {
+ if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template))
+ return false;
+
+ $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template)
+ ? DATA_ROOT . '/www/squelettes/' . $template
+ : ROOT . '/www/squelettes-dist/' . $template;
+
+ if (!file_exists($path))
+ return false;
+
+ return file_get_contents($path);
+ }
+
+ static public function editSource($template, $content)
+ {
+ if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template))
+ return false;
+
+ $path = DATA_ROOT . '/www/squelettes/' . $template;
+
+ return file_put_contents($path, $content);
+ }
+
+ static public function resetSource($template)
+ {
+ if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template))
+ return false;
+
+ if (file_exists(DATA_ROOT . '/www/squelettes/' . $template))
+ {
+ unlink(DATA_ROOT . '/www/squelettes/' . $template);
+ }
+
+ return true;
+ }
+
+ static public function listSources()
+ {
+ if (!file_exists(DATA_ROOT . '/www/squelettes'))
+ {
+ mkdir(DATA_ROOT . '/www/squelettes');
+ }
+
+ $sources = [];
+
+ $dir = dir(ROOT . '/www/squelettes-dist');
+
+ while ($file = $dir->read())
+ {
+ if ($file[0] == '.')
+ continue;
+
+ if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file))
+ continue;
+
+ $sources[] = $file;
+ }
+
+ $dir->close();
+
+ $dir = dir(DATA_ROOT . '/www/squelettes');
+
+ while ($file = $dir->read())
+ {
+ if ($file[0] == '.')
+ continue;
+
+ if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file))
+ continue;
+
+ $sources[] = $file;
+ }
+
+ $dir->close();
+
+ $sources = array_unique($sources);
+ sort($sources);
+
+ return $sources;
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/include/class.wiki.php b/include/class.wiki.php
new file mode 100644
index 0000000..3b66544
--- /dev/null
+++ b/include/class.wiki.php
@@ -0,0 +1,528 @@
+simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE id = ?;', false, $data['parent']))
+ {
+ $data['parent'] = 0;
+ }
+ }
+
+ return true;
+ }
+
+ public function create($data = [])
+ {
+ $this->_checkFields($data);
+ $db = DB::getInstance();
+
+ if (!empty($data['uri']))
+ {
+ $data['uri'] = self::transformTitleToURI($data['uri']);
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', false, $data['uri']))
+ {
+ throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.');
+ }
+ }
+ else
+ {
+ $data['uri'] = self::transformTitleToURI($data['titre']);
+
+ if (!trim($data['uri']) || $db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', false, $data['uri']))
+ {
+ $data['uri'] .= '_' . date('d-m-Y_H-i-s');
+ }
+ }
+
+ $db->simpleInsert('wiki_pages', $data);
+ $id = $db->lastInsertRowId();
+
+ // On ne peut utiliser un trigger pour insérer dans la recherche
+ // car les tables virtuelles font des opérations qui modifient
+ // last_insert_rowid() et donc résultat incohérent
+ $db->simpleInsert('wiki_recherche', ['id' => $id, 'titre' => $data['titre']]);
+
+ return $id;
+ }
+
+ public function edit($id, $data = [])
+ {
+ $db = DB::getInstance();
+ $this->_checkFields($data);
+
+ if (isset($data['uri']))
+ {
+ $data['uri'] = self::transformTitleToURI($data['uri']);
+
+ if ($db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? AND id != ? LIMIT 1;', false, $data['uri'], (int)$id))
+ {
+ throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.');
+ }
+ }
+
+ if (isset($data['droit_lecture']) && $data['droit_lecture'] >= self::LECTURE_CATEGORIE)
+ {
+ $data['droit_ecriture'] = $data['droit_lecture'];
+ }
+
+ if (isset($data['parent']) && (int)$data['parent'] == (int)$id)
+ {
+ $data['parent'] = 0;
+ }
+
+ $data['date_modification'] = gmdate('Y-m-d H:i:s');
+
+ // Modification de la date de création
+ if (isset($data['date_creation']))
+ {
+ // Si la date n'est pas valide tant pis
+ if (!(strtotime($data['date_creation']) > 0))
+ {
+ unset($data['date_creation']);
+ }
+ else
+ {
+ $data['date_creation'] = gmdate('Y-m-d H:i:s', $data['date_creation']);
+ }
+ }
+
+ $db->simpleUpdate('wiki_pages', $data, 'id = '.(int)$id);
+ return true;
+ }
+
+ public function delete($id)
+ {
+ $db = DB::getInstance();
+
+ if ($db->simpleQuerySingle('SELECT COUNT(*) FROM wiki_pages WHERE parent = ?;', false, (int)$id))
+ {
+ return false;
+ }
+
+ $db->simpleExec('DELETE FROM wiki_revisions WHERE id_page = ?;', (int)$id);
+ //$db->simpleExec('DELETE FROM wiki_suivi WHERE id_page = ?;', (int)$id); FIXME
+ $db->simpleExec('DELETE FROM wiki_recherche WHERE id = ?;', (int)$id);
+ $db->simpleExec('DELETE FROM wiki_pages WHERE id = ?;', (int)$id);
+ return true;
+ }
+
+ public function get($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT *,
+ strftime(\'%s\', date_creation) AS date_creation,
+ strftime(\'%s\', date_modification) AS date_modification
+ FROM wiki_pages WHERE id = ? LIMIT 1;', true, (int)$id);
+ }
+
+ public function getTitle($id)
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id);
+ }
+
+ public function getRevision($id, $rev)
+ {
+ $db = DB::getInstance();
+ $champ_id = Config::getInstance()->get('champ_identite');
+
+ // FIXME pagination au lieu de bloquer à 1000
+ return $db->simpleQuerySingle('SELECT r.revision, r.modification, r.id_auteur, r.contenu,
+ strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur,
+ r.chiffrement
+ FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur
+ WHERE r.id_page = ? AND revision = ? LIMIT 1;', true, (int) $id, (int) $rev);
+ }
+
+ public function listRevisions($id)
+ {
+ $db = DB::getInstance();
+ $champ_id = Config::getInstance()->get('champ_identite');
+
+ // FIXME pagination au lieu de bloquer à 1000
+ return $db->simpleStatementFetch('SELECT r.revision, r.modification, r.id_auteur,
+ strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur,
+ LENGTH(r.contenu) - (SELECT LENGTH(contenu) FROM wiki_revisions WHERE id_page = r.id_page AND revision < r.revision ORDER BY revision DESC LIMIT 1)
+ AS diff_taille, r.chiffrement
+ FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur
+ WHERE r.id_page = ? ORDER BY r.revision DESC LIMIT 1000;', SQLITE3_ASSOC, (int) $id);
+ }
+
+ public function editRevision($id, $revision_edition = 0, $data)
+ {
+ $db = DB::getInstance();
+
+ $revision = $db->simpleQuerySingle('SELECT revision FROM wiki_pages WHERE id = ?;', false, (int)$id);
+
+ // ?! L'ID fournit ne correspond à rien ?
+ if ($revision === false)
+ {
+ throw new \RuntimeException('La page demandée n\'existe pas.');
+ }
+
+ // Pas de révision
+ if ($revision == 0 && !trim($data['contenu']))
+ {
+ return true;
+ }
+
+ // Il faut obligatoirement fournir un ID d'auteur
+ if (empty($data['id_auteur']) && $data['id_auteur'] !== null)
+ {
+ throw new \BadMethodCallException('Aucun ID auteur de fourni.');
+ }
+
+ $contenu = $db->simpleQuerySingle('SELECT contenu FROM wiki_revisions WHERE revision = ? AND id_page = ?;', false, (int)$revision, (int)$id);
+
+ // Pas de changement au contenu, pas la peine d'enregistrer une nouvelle révision
+ if (trim($contenu) == trim($data['contenu']))
+ {
+ return true;
+ }
+
+ // Révision sur laquelle est basée la nouvelle révision
+ // utilisé pour vérifier que le contenu n'a pas été modifié depuis qu'on
+ // a chargé la page d'édition
+ if ($revision > $revision_edition)
+ {
+ throw new UserException('La page a été modifiée depuis le début de votre modification.');
+ }
+
+ if (empty($data['chiffrement']))
+ $data['chiffrement'] = 0;
+
+ if (!isset($data['modification']) || !trim($data['modification']))
+ $data['modification'] = null;
+
+ // Incrémentons le numéro de révision
+ $revision++;
+
+ $data['id_page'] = $id;
+ $data['revision'] = $revision;
+
+ $db->simpleInsert('wiki_revisions', $data);
+ $db->simpleUpdate('wiki_pages', [
+ 'revision' => $revision,
+ 'date_modification' => gmdate('Y-m-d H:i:s'),
+ ], 'id = '.(int)$id);
+
+ return true;
+ }
+
+ public function search($query)
+ {
+ $db = DB::getInstance();
+ return $db->simpleStatementFetch('SELECT
+ p.uri, r.*, snippet(wiki_recherche, \'\', \' \', \'...\', -1, -50) AS snippet,
+ rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points
+ FROM wiki_recherche AS r INNER JOIN wiki_pages AS p ON p.id = r.id
+ WHERE '.$this->_getLectureClause('p.').' AND wiki_recherche MATCH \''.$db->escapeString($query).'\'
+ ORDER BY points DESC LIMIT 0,50;');
+ }
+
+ public function setRestrictionCategorie($id, $droit_wiki)
+ {
+ $this->restriction_categorie = $id;
+ $this->restriction_droit = $droit_wiki;
+ return true;
+ }
+
+ protected function _getLectureClause($prefix = '')
+ {
+ if (is_null($this->restriction_categorie))
+ {
+ throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.');
+ }
+
+ if ($this->restriction_droit == Membres::DROIT_AUCUN)
+ {
+ throw new UserException('Vous n\'avez pas accès au wiki.');
+ }
+
+ if ($this->restriction_droit == Membres::DROIT_ADMIN)
+ return '1';
+
+ return '('.$prefix.'droit_lecture = '.self::LECTURE_NORMAL.' OR '.$prefix.'droit_lecture = '.self::LECTURE_PUBLIC.'
+ OR '.$prefix.'droit_lecture = '.(int)$this->restriction_categorie.')';
+ }
+
+ public function canReadPage($lecture)
+ {
+ if (is_null($this->restriction_categorie))
+ {
+ throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.');
+ }
+
+ if ($this->restriction_droit < Membres::DROIT_ACCES)
+ {
+ return false;
+ }
+
+ if ($this->restriction_droit == Membres::DROIT_ADMIN
+ || $lecture == self::LECTURE_NORMAL || $lecture == self::LECTURE_PUBLIC
+ || $lecture == $this->restriction_categorie)
+ return true;
+
+ return false;
+ }
+
+ public function canWritePage($ecriture)
+ {
+ if (is_null($this->restriction_categorie))
+ {
+ throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.');
+ }
+
+ if ($this->restriction_droit < Membres::DROIT_ECRITURE)
+ {
+ return false;
+ }
+
+ if ($this->restriction_droit == Membres::DROIT_ADMIN
+ || $ecriture == self::ECRITURE_NORMAL
+ || $ecriture == $this->restriction_categorie)
+ return true;
+
+ return false;
+ }
+
+ public function getList($parent = 0)
+ {
+ $db = DB::getInstance();
+
+ return $db->simpleStatementFetch(
+ 'SELECT id, revision, uri, titre,
+ strftime(\'%s\', date_creation) AS date_creation,
+ strftime(\'%s\', date_modification) AS date_modification
+ FROM wiki_pages
+ WHERE parent = ? AND '.$this->_getLectureClause().'
+ ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE LIMIT 500;',
+ SQLITE3_ASSOC,
+ (int) $parent
+ );
+ }
+
+ public function getById($id)
+ {
+ $db = DB::getInstance();
+ $page = $db->simpleQuerySingle('SELECT *,
+ strftime(\'%s\', date_creation) AS date_creation,
+ strftime(\'%s\', date_modification) AS date_modification
+ FROM wiki_pages
+ WHERE id = ?;', true, (int)$id);
+
+ if (!$page)
+ {
+ return false;
+ }
+
+ if ($page['revision'] > 0)
+ {
+ $page['contenu'] = $db->simpleQuerySingle('SELECT * FROM wiki_revisions
+ WHERE id_page = ? AND revision = ?;', true, (int)$page['id'], (int)$page['revision']);
+ }
+ else
+ {
+ $page['contenu'] = false;
+ }
+
+ return $page;
+ }
+
+ public function getByURI($uri)
+ {
+ $db = DB::getInstance();
+ $page = $db->simpleQuerySingle('SELECT *,
+ strftime(\'%s\', date_creation) AS date_creation,
+ strftime(\'%s\', date_modification) AS date_modification
+ FROM wiki_pages
+ WHERE uri = ?;', true, trim($uri));
+
+ if (!$page)
+ {
+ return false;
+ }
+
+ if ($page['revision'] > 0)
+ {
+ $page['contenu'] = $db->simpleQuerySingle('SELECT * FROM wiki_revisions
+ WHERE id_page = ? AND revision = ?;', true, (int)$page['id'], (int)$page['revision']);
+ }
+ else
+ {
+ $page['contenu'] = false;
+ }
+
+ return $page;
+ }
+
+ public function listRecentModifications($page = 1)
+ {
+ $begin = ($page - 1) * self::ITEMS_PER_PAGE;
+
+ $db = DB::getInstance();
+
+ return $db->simpleStatementFetch('SELECT *,
+ strftime(\'%s\', date_creation) AS date_creation,
+ strftime(\'%s\', date_modification) AS date_modification
+ FROM wiki_pages
+ WHERE '.$this->_getLectureClause().'
+ ORDER BY date_modification DESC;', SQLITE3_ASSOC);
+ }
+
+ public function countRecentModifications()
+ {
+ $db = DB::getInstance();
+ return $db->simpleQuerySingle('SELECT COUNT(*) FROM wiki_pages WHERE '.$this->_getLectureClause().';');
+ }
+
+ public function listBackBreadCrumbs($id)
+ {
+ if ($id == 0)
+ return [];
+
+ $db = DB::getInstance();
+ $flat = [];
+
+ while ($id > 0)
+ {
+ $res = $db->simpleQuerySingle('SELECT parent, titre, uri
+ FROM wiki_pages WHERE id = ? LIMIT 1;', true, (int)$id);
+
+ $flat[] = [
+ 'id' => $id,
+ 'titre' => $res['titre'],
+ 'uri' => $res['uri'],
+ ];
+
+ $id = (int)$res['parent'];
+ }
+
+ return array_reverse($flat);
+ }
+
+ public function listBackParentTree($id)
+ {
+ $db = DB::getInstance();
+ $flat = [
+ [
+ 'id' => 0,
+ 'parent' => null,
+ 'titre' => 'Racine',
+ 'children' => $db->simpleStatementFetchAssocKey('SELECT id, parent, titre FROM wiki_pages
+ WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;',
+ SQLITE3_ASSOC, 0)
+ ]
+ ];
+
+ do
+ {
+ $parent = $db->simpleQuerySingle('SELECT parent FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id);
+
+ $flat[$id] = [
+ 'id' => $id,
+ 'parent' => $id ? (int)$parent : null,
+ 'titre' => $id ? (string)$db->simpleQuerySingle('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id) : 'Racine',
+ 'children' => $db->simpleStatementFetchAssocKey('SELECT id, parent, titre FROM wiki_pages
+ WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;',
+ SQLITE3_ASSOC, (int)$id)
+ ];
+
+ $id = (int)$parent;
+ }
+ while ($id != 0);
+
+ $tree = [];
+ foreach ($flat as $id=>&$node)
+ {
+ if (is_null($node['parent']))
+ {
+ $tree[$id] = &$node;
+ }
+ else
+ {
+ if (!isset($flat[$node['parent']]['children']))
+ {
+ $flat[$node['parent']]['children'] = [];
+ }
+
+ $flat[$node['parent']]['children'][$id] = &$node;
+ }
+ }
+
+ return $tree;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/data/0.4.0.sql b/include/data/0.4.0.sql
new file mode 100644
index 0000000..33b7e53
--- /dev/null
+++ b/include/data/0.4.0.sql
@@ -0,0 +1,103 @@
+CREATE TABLE compta_exercices
+-- Exercices
+(
+ id INTEGER PRIMARY KEY,
+
+ libelle TEXT NOT NULL,
+
+ debut TEXT NOT NULL DEFAULT CURRENT_DATE,
+ fin TEXT NULL DEFAULT NULL,
+
+ clos INTEGER NOT NULL DEFAULT 0
+);
+
+
+CREATE TABLE compta_comptes
+-- Plan comptable
+(
+ id TEXT PRIMARY KEY,
+ parent TEXT NOT NULL DEFAULT 0,
+
+ libelle TEXT NOT NULL,
+
+ position INTEGER NOT NULL, -- position actif/passif/charge/produit
+ plan_comptable INTEGER NOT NULL DEFAULT 1 -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
+);
+
+CREATE INDEX compta_comptes_parent ON compta_comptes (parent);
+
+CREATE TABLE compta_comptes_bancaires
+-- Comptes bancaires
+(
+ id TEXT PRIMARY KEY,
+
+ banque TEXT NOT NULL,
+
+ iban TEXT,
+ bic TEXT,
+
+ FOREIGN KEY(id) REFERENCES compta_comptes(id)
+);
+
+CREATE TABLE compta_journal
+-- Journal des opérations comptables
+(
+ id INTEGER PRIMARY KEY,
+
+ libelle TEXT NOT NULL,
+ remarques TEXT,
+ numero_piece TEXT, -- N° de pièce comptable
+
+ montant REAL,
+
+ date TEXT DEFAULT CURRENT_DATE,
+ moyen_paiement TEXT DEFAULT NULL,
+ numero_cheque TEXT DEFAULT NULL,
+
+ compte_debit INTEGER, -- N° du compte dans le plan
+ compte_credit INTEGER, -- N° du compte dans le plan
+
+ id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
+ id_auteur INTEGER NOT NULL,
+ id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
+
+ FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
+ FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
+ FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
+ FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
+ FOREIGN KEY(id_auteur) REFERENCES membres(id),
+ FOREIGN KEY(id_categorie) REFERENCES compta_categories(id)
+);
+
+CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice);
+CREATE INDEX compta_operations_date ON compta_journal (date);
+CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
+CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur);
+
+CREATE TABLE compta_moyens_paiement
+-- Moyens de paiement
+(
+ code TEXT PRIMARY KEY,
+ nom TEXT
+);
+
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');
+
+CREATE TABLE compta_categories
+-- Catégories pour simplifier le plan comptable
+(
+ id INTEGER PRIMARY KEY,
+ type INTEGER DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)
+
+ intitule TEXT NOT NULL,
+ description TEXT,
+
+ compte TEXT NOT NULL, -- Compte affecté par cette catégorie
+
+ FOREIGN KEY(compte) REFERENCES compta_comptes(id)
+);
\ No newline at end of file
diff --git a/include/data/0.4.3.sql b/include/data/0.4.3.sql
new file mode 100644
index 0000000..2e73272
--- /dev/null
+++ b/include/data/0.4.3.sql
@@ -0,0 +1,79 @@
+DROP TABLE compta_exercices;
+
+CREATE TABLE compta_exercices
+-- Exercices
+(
+ id INTEGER PRIMARY KEY,
+
+ libelle TEXT NOT NULL,
+
+ debut TEXT NOT NULL DEFAULT CURRENT_DATE,
+ fin TEXT NULL DEFAULT NULL,
+
+ cloture INTEGER NOT NULL DEFAULT 0
+);
+
+INSERT INTO compta_exercices (libelle, debut, fin, cloture)
+ VALUES (
+ 'Premier exercice',
+ (CASE WHEN
+ (SELECT strftime('%Y-01-01', date) FROM compta_journal ORDER BY date ASC LIMIT 1)
+ IS NOT NULL THEN (SELECT strftime('%Y-01-01', date) FROM compta_journal ORDER BY date ASC LIMIT 1)
+ ELSE strftime('%Y-01-01', 'now') END
+ ),
+ (CASE WHEN
+ (SELECT strftime('%Y-12-31', date) FROM compta_journal ORDER BY date DESC LIMIT 1)
+ IS NOT NULL THEN (SELECT strftime('%Y-12-31', date) FROM compta_journal ORDER BY date DESC LIMIT 1)
+ ELSE strftime('%Y-12-31', 'now') END
+ ),
+ 0
+ );
+
+BEGIN;
+ALTER TABLE compta_journal RENAME TO old_compta_journal;
+DROP INDEX compta_operations_exercice;
+DROP INDEX compta_operations_date;
+DROP INDEX compta_operations_comptes;
+DROP INDEX compta_operations_auteur;
+
+CREATE TABLE compta_journal
+-- Journal des opérations comptables
+(
+ id INTEGER PRIMARY KEY,
+
+ libelle TEXT NOT NULL,
+ remarques TEXT,
+ numero_piece TEXT, -- N° de pièce comptable
+
+ montant REAL,
+
+ date TEXT DEFAULT CURRENT_DATE,
+ moyen_paiement TEXT DEFAULT NULL,
+ numero_cheque TEXT DEFAULT NULL,
+
+ compte_debit INTEGER, -- N° du compte dans le plan
+ compte_credit INTEGER, -- N° du compte dans le plan
+
+ id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
+ id_auteur INTEGER NULL,
+ id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
+
+ FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
+ FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
+ FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
+ FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
+ FOREIGN KEY(id_auteur) REFERENCES membres(id),
+ FOREIGN KEY(id_categorie) REFERENCES compta_categories(id)
+);
+
+CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice);
+CREATE INDEX compta_operations_date ON compta_journal (date);
+CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
+CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur);
+
+INSERT INTO compta_journal SELECT * FROM old_compta_journal;
+
+UPDATE compta_journal SET id_exercice = 1;
+
+DROP TABLE old_compta_journal;
+END;
\ No newline at end of file
diff --git a/include/data/0.6.0.sql b/include/data/0.6.0.sql
new file mode 100644
index 0000000..543e50e
--- /dev/null
+++ b/include/data/0.6.0.sql
@@ -0,0 +1,110 @@
+CREATE TABLE cotisations
+-- Types de cotisations et activités
+(
+ id INTEGER PRIMARY KEY,
+ id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta
+
+ intitule TEXT NOT NULL,
+ description TEXT NULL,
+ montant REAL NOT NULL,
+
+ duree INTEGER NULL, -- En jours
+ debut TEXT NULL, -- timestamp
+ fin TEXT NULL,
+
+ FOREIGN KEY (id_categorie_compta) REFERENCES compta_categories (id)
+);
+
+CREATE TABLE cotisations_membres
+-- Enregistrement des cotisations et activités
+(
+ id INTEGER NOT NULL PRIMARY KEY,
+ id_membre INTEGER NOT NULL REFERENCES membres (id),
+ id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
+
+ date TEXT NOT NULL DEFAULT CURRENT_DATE
+);
+
+CREATE UNIQUE INDEX cm_unique ON cotisations_membres (id_membre, id_cotisation, date);
+
+CREATE TABLE membres_operations
+-- Liaision des enregistrement des paiements en compta
+(
+ id_membre INTEGER NOT NULL REFERENCES membres (id),
+ id_operation INTEGER NOT NULL REFERENCES compta_journal (id),
+ id_cotisation INTEGER NULL REFERENCES cotisations_membres (id),
+
+ PRIMARY KEY (id_membre, id_operation)
+);
+
+CREATE TABLE rappels
+-- Rappels de devoir renouveller une cotisation
+(
+ id INTEGER PRIMARY KEY,
+ id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
+
+ delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel
+
+ sujet TEXT NOT NULL,
+ texte TEXT NOT NULL
+);
+
+CREATE TABLE rappels_envoyes
+-- Enregistrement des rappels envoyés à qui et quand
+(
+ id INTEGER PRIMARY KEY,
+
+ id_membre INTEGER NOT NULL REFERENCES membres (id),
+ id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
+
+ date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
+);
+
+CREATE TABLE plugins
+-- Plugins / extensions
+(
+ id TEXT PRIMARY KEY,
+ officiel INTEGER NOT NULL DEFAULT 0,
+ nom TEXT NOT NULL,
+ description TEXT,
+ auteur TEXT,
+ url TEXT,
+ version TEXT NOT NULL,
+ menu INTEGER NOT NULL DEFAULT 0,
+ config TEXT
+);
+
+-- Mise à jour des catégories
+
+CREATE TABLE membres_categories_tmp
+-- Catégories de membres
+(
+ id INTEGER PRIMARY KEY,
+ nom TEXT,
+ description TEXT,
+
+ droit_wiki INT DEFAULT 1,
+ droit_membres INT DEFAULT 1,
+ droit_compta INT DEFAULT 1,
+ droit_inscription INT DEFAULT 0,
+ droit_connexion INT DEFAULT 1,
+ droit_config INT DEFAULT 0,
+ cacher INT DEFAULT 0,
+
+ id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id)
+);
+
+-- Remise des anciennes infos
+INSERT INTO membres_categories_tmp SELECT id, nom, description, droit_wiki, droit_membres,
+ droit_compta, droit_inscription, droit_connexion, droit_config, cacher, NULL FROM membres_categories;
+
+-- Suppression de l'ancienne table et renommage de la nouvelle
+DROP TABLE membres_categories;
+ALTER TABLE membres_categories_tmp RENAME TO membres_categories;
+
+-- Ajout désactivation compte
+ALTER TABLE compta_comptes ADD COLUMN desactive INTEGER NOT NULL DEFAULT 0;
+
+PRAGMA foreign_keys = ON;
\ No newline at end of file
diff --git a/include/data/categories_comptables.sql b/include/data/categories_comptables.sql
new file mode 100644
index 0000000..80d0d48
--- /dev/null
+++ b/include/data/categories_comptables.sql
@@ -0,0 +1,22 @@
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Prestations de service','','604');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Achat de marchandises à vendre','Marchandises destinées à être revendues en l''état.','607');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Achat de fournitures consommables','','6068');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Publicité et relations publiques','','623');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais de déplacement des membres','Billet SNCF, remboursement de frais kilométrique, etc.','625');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Locations','Locations versées pour un local ou du matériel.','613');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Fournitures non stockables : eau, électricité...','Facture d''eau, d''opérateur électrique, etc.','6061');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Fournitures administratives','Cartouches d''encre, papier, matériel bureautique, etc.','6064');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais d''actes et de contentieux','Insertion au Journal Officiel, frais de justice, etc.','6227');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Frais postaux et télécommunications','Facture d''accès à Internet, timbres, etc.','626');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Prime d''assurance','','616');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Services bancaires','','627');
+INSERT INTO "compta_categories" VALUES(NULL,-1,'Divers','','658');
+
+INSERT INTO "compta_categories" VALUES(NULL,1,'Vente de produits finis','Vente de produits fabriqués par l''association.','701');
+INSERT INTO "compta_categories" VALUES(NULL,1,'Prestation de service','','706');
+INSERT INTO "compta_categories" VALUES(NULL,1,'Revente de marchandises','','707');
+INSERT INTO "compta_categories" VALUES(NULL,1,'Manifestations diverses','Revenus provenant de manifestations au profit de l''association : droit d''entrée, location d''emplacement en vide grenier, ventes, etc.','7780');
+INSERT INTO "compta_categories" VALUES(NULL,1,'Cotisations','','756');
+INSERT INTO "compta_categories" VALUES(NULL,1,'Dons et collectes','','754');
+INSERT INTO "compta_categories" VALUES(NULL,1,'Subventions','','740');
+INSERT INTO "compta_categories" VALUES(NULL,1,'Divers','','758');
diff --git a/include/data/champs_membres.ini b/include/data/champs_membres.ini
new file mode 100644
index 0000000..d68e352
--- /dev/null
+++ b/include/data/champs_membres.ini
@@ -0,0 +1,129 @@
+; Ce fichier contient la configuration par défaut des champs des fiches membres.
+; La configuration est ensuite enregistrée au format INI dans la table
+; config de la base de données.
+;
+; Syntaxe :
+;
+; [nom_du_champ] ; Nom unique du champ, ne peut contenir que des lettres et des tirets bas
+; type = text
+; title = "Super champ trop cool"
+; mandatory = true
+; editable = false
+;
+; Description des options possibles pour chaque champ :
+;
+; type: (défaut: text) OBLIGATOIRE
+; certains types gérés par de HTML5 :
+; text, number, date, datetime, url, email, checkbox, file, password, tel
+; champs spécifiques :
+; - country = sélecteur de pays
+; - textarea = texte multi lignes
+; - multiple = multiples cases à cocher (jusqu'à 32, binaire)
+; - select = un choix parmis plusieurs
+; title: OBLIGATOIRE
+; Titre du champ
+; help:
+; Texte d'aide sur les fiches membres
+; options[]:
+; pour définir les options d'un champ de type select ou multiple
+; editable:
+; true = modifiable par le membre
+; false = modifiable uniquement par un admin (défaut)
+; mandatory:
+; true = obligatoire, la fiche membre ne pourra être enregistrée si ce champ est vide
+; false = facultatif (défaut)
+; private:
+; true = non visible par le membre lui-même
+; false = visible par le membre (défaut)
+; list_row:
+; Si absent ou zéro ('0') ou false, ce champ n'apparaîtra pas dans la liste des membres
+; Si présent et un chiffre supérieur à 0, alors le champ apparaîtra dans la liste des membres
+; dans l'ordre défini par le chiffre (si nom est à 2 et email à 1, alors email sera
+; la première colonne et nom la seconde)
+; install:
+; true = sera ajouté aux fiches membres à l'installation
+; false = sera seulement présent dans les champs supplémentaires possibles (défaut)
+
+[nom]
+type = text
+title = "Nom & prénom"
+mandatory = true
+install = true
+editable = true
+list_row = 1
+
+[email]
+; ce champ est obligatoirement présent et de type 'email'
+type = email
+title = "Adresse E-Mail"
+mandatory = true
+install = true
+editable = true
+
+[passe]
+; ce champ est obligatoirement présent et de type 'password'
+; le titre ne peut être modifié
+type = password
+mandatory = true
+install = true
+editable = true
+
+[adresse]
+type = textarea
+title = "Adresse postale"
+help = "Indiquer ici le numéro, le type de voie, etc."
+install = true
+editable = true
+
+[code_postal]
+type = text
+title = "Code postal"
+install = true
+editable = true
+list_row = 2
+
+[ville]
+type = text
+title = "Ville"
+install = true
+editable = true
+list_row = 3
+
+[pays]
+type = country
+title = "Pays"
+install = true
+editable = true
+
+[telephone]
+type = tel
+title = "Numéro de téléphone"
+install = true
+editable = true
+
+[lettre_infos]
+type = checkbox
+title = "Inscription à la lettre d'information"
+install = true
+editable = true
+
+[groupe_travail]
+type = multiple
+title = "Groupes de travail"
+editable = false
+options[] = "Télécoms"
+options[] = "Trésorerie"
+options[] = "Relations publiques"
+options[] = "Communication presse"
+options[] = "Organisation d'événements"
+
+[date_naissance]
+type = date
+title = "Date de naissance"
+editable = true
+
+[notes]
+type = textarea
+title = "Notes"
+editable = false
+private = true
diff --git a/include/data/plan_comptable.json b/include/data/plan_comptable.json
new file mode 100644
index 0000000..b249b85
--- /dev/null
+++ b/include/data/plan_comptable.json
@@ -0,0 +1,1718 @@
+{
+ "1": {
+ "code": 1,
+ "nom": "Classe 1 \u2014 Comptes de capitaux (Fonds propres, emprunts et dettes assimil\u00e9s)",
+ "parent": 0,
+ "position": 1
+ },
+ "10": {
+ "code": 10,
+ "nom": "FONDS ASSOCIATIFS ET R\u00c9SERVES",
+ "parent": 1,
+ "position": 1
+ },
+ "102": {
+ "code": 102,
+ "nom": "Fonds associatif sans droit de reprise",
+ "parent": 10,
+ "position": 1
+ },
+ "1021": {
+ "code": 1021,
+ "nom": "Valeur du patrimoine int\u00e9gr\u00e9",
+ "parent": 102,
+ "position": 1
+ },
+ "1022": {
+ "code": 1022,
+ "nom": "Fonds statutaire",
+ "parent": 102,
+ "position": 1
+ },
+ "1024": {
+ "code": 1024,
+ "nom": "Apports sans droit de reprise",
+ "parent": 102,
+ "position": 1
+ },
+ "103": {
+ "code": 103,
+ "nom": "Fonds associatif avec droit de reprise",
+ "parent": 10,
+ "position": 1
+ },
+ "1034": {
+ "code": 1034,
+ "nom": "Apports avec droit de reprise",
+ "parent": 103,
+ "position": 1
+ },
+ "105": {
+ "code": 105,
+ "nom": "\u00c9carts de r\u00e9\u00e9valuation",
+ "parent": 10,
+ "position": 1
+ },
+ "106": {
+ "code": 106,
+ "nom": "R\u00e9serves",
+ "parent": 10,
+ "position": 1
+ },
+ "1063": {
+ "code": 1063,
+ "nom": "R\u00e9serves statutaires ou contractuelles",
+ "parent": 106,
+ "position": 1
+ },
+ "1064": {
+ "code": 1064,
+ "nom": "R\u00e9serves r\u00e9glement\u00e9es",
+ "parent": 106,
+ "position": 1
+ },
+ "1068": {
+ "code": 1068,
+ "nom": "Autres r\u00e9serves (dont r\u00e9serves pour projet associatif)",
+ "parent": 106,
+ "position": 1
+ },
+ "11": {
+ "code": 11,
+ "nom": "REPORT \u00c0 NOUVEAU",
+ "parent": 1,
+ "position": 1
+ },
+ "110": {
+ "code": 110,
+ "nom": "Report \u00e0 nouveau (Solde cr\u00e9diteur)",
+ "parent": 11,
+ "position": 1
+ },
+ "119": {
+ "code": 119,
+ "nom": "Report \u00e0 nouveau (Solde d\u00e9biteur)",
+ "parent": 11,
+ "position": 1
+ },
+ "12": {
+ "code": 12,
+ "nom": "R\u00c9SULTAT NET DE L'EXERCICE",
+ "parent": 1,
+ "position": 1
+ },
+ "120": {
+ "code": 120,
+ "nom": "R\u00e9sultat de l'exercice (exc\u00e9dent)",
+ "parent": 12,
+ "position": 1
+ },
+ "129": {
+ "code": 129,
+ "nom": "R\u00e9sultat de l'exercice (d\u00e9ficit)",
+ "parent": 12,
+ "position": 1
+ },
+ "13": {
+ "code": 13,
+ "nom": "SUBVENTIONS D'INVESTISSEMENT AFFECT\u00c9ES A DES BIENS NON RENOUVELABLES",
+ "parent": 1,
+ "position": 1
+ },
+ "131": {
+ "code": 131,
+ "nom": "Subventions d'investissement (renouvelables)",
+ "parent": 13,
+ "position": 1
+ },
+ "139": {
+ "code": 139,
+ "nom": "Subventions d'investissement inscrites au compte de r\u00e9sultat",
+ "parent": 13,
+ "position": 1
+ },
+ "14": {
+ "code": 14,
+ "nom": "PROVISIONS REGLEMENT\u00c9ES",
+ "parent": 1,
+ "position": 1
+ },
+ "15": {
+ "code": 15,
+ "nom": "PROVISIONS",
+ "parent": 1,
+ "position": 1
+ },
+ "151": {
+ "code": 151,
+ "nom": "Provisions pour risques",
+ "parent": 15,
+ "position": 1
+ },
+ "157": {
+ "code": 157,
+ "nom": "Provisions pour charges \u00e0 r\u00e9partir sur plusieurs exercices",
+ "parent": 15,
+ "position": 1
+ },
+ "158": {
+ "code": 158,
+ "nom": "Autres provisions pour charges",
+ "parent": 15,
+ "position": 1
+ },
+ "16": {
+ "code": 16,
+ "nom": "EMPRUNTS ET DETTES ASSIMIL\u00c9ES",
+ "parent": 1,
+ "position": 1
+ },
+ "164": {
+ "code": 164,
+ "nom": "Emprunts aupr\u00e8s des \u00e9tablissements de cr\u00e9dits",
+ "parent": 16,
+ "position": 1
+ },
+ "165": {
+ "code": 165,
+ "nom": "D\u00e9p\u00f4ts et cautionnements re\u00e7us",
+ "parent": 16,
+ "position": 1
+ },
+ "167": {
+ "code": 167,
+ "nom": "Emprunts et dettes assorties de conditions particuli\u00e8res",
+ "parent": 16,
+ "position": 1
+ },
+ "168": {
+ "code": 168,
+ "nom": "Autres emprunts et dettes assimil\u00e9s",
+ "parent": 16,
+ "position": 1
+ },
+ "17": {
+ "code": 17,
+ "nom": "DETTES RATTACH\u00c9ES \u00c0 DES PARTICIPATIONS",
+ "parent": 1,
+ "position": 1
+ },
+ "18": {
+ "code": 18,
+ "nom": "COMPTES DE LIAISON DES \u00c9TABLISSEMENTS",
+ "parent": 1,
+ "position": 1
+ },
+ "181": {
+ "code": 181,
+ "nom": "Apports permanents entre si\u00e8ge social et \u00e9tablissements",
+ "parent": 18,
+ "position": 1
+ },
+ "185": {
+ "code": 185,
+ "nom": "Biens et prestations de services \u00e9chang\u00e9s entre \u00e9tablissements et si\u00e8ge social",
+ "parent": 18,
+ "position": 1
+ },
+ "186": {
+ "code": 186,
+ "nom": "Biens et prestations de services \u00e9chang\u00e9s entre \u00e9tablissements (charges)",
+ "parent": 18,
+ "position": 1
+ },
+ "187": {
+ "code": 187,
+ "nom": "Biens et prestations de services \u00e9chang\u00e9s entre \u00e9tablissements (produits)",
+ "parent": 18,
+ "position": 1
+ },
+ "19": {
+ "code": 19,
+ "nom": "FONDS D\u00c9DI\u00c9S",
+ "parent": 1,
+ "position": 1
+ },
+ "194": {
+ "code": 194,
+ "nom": "Fonds d\u00e9di\u00e9s sur subventions de fonctionnement",
+ "parent": 19,
+ "position": 1
+ },
+ "195": {
+ "code": 195,
+ "nom": "Fonds d\u00e9di\u00e9s sur dons manuels affect\u00e9s",
+ "parent": 19,
+ "position": 1
+ },
+ "197": {
+ "code": 197,
+ "nom": "Fonds d\u00e9di\u00e9s sur legs et donations affect\u00e9s",
+ "parent": 19,
+ "position": 1
+ },
+ "198": {
+ "code": 198,
+ "nom": "Exc\u00e9dent disponible apr\u00e8s affectation au projet associatif",
+ "parent": 19,
+ "position": 1
+ },
+ "199": {
+ "code": 199,
+ "nom": "Reprise des fonds affect\u00e9s au projet associatif",
+ "parent": 19,
+ "position": 1
+ },
+ "2": {
+ "code": 2,
+ "nom": "Classe 2 \u2014 Comptes d'immobilisations",
+ "parent": 0,
+ "position": 2
+ },
+ "20": {
+ "code": 20,
+ "nom": "IMMOBILISATIONS INCORPORELLES",
+ "parent": 2,
+ "position": 2
+ },
+ "200": {
+ "code": 200,
+ "nom": "Immobilisations incorporelles",
+ "parent": 20,
+ "position": 2
+ },
+ "21": {
+ "code": 21,
+ "nom": "IMMOBILISATIONS CORPORELLES",
+ "parent": 2,
+ "position": 2
+ },
+ "210": {
+ "code": 210,
+ "nom": "Investissements",
+ "parent": 21,
+ "position": 2
+ },
+ "22": {
+ "code": 22,
+ "nom": "IMMOBILISATIONS GREV\u00c9ES DE DROITS",
+ "parent": 2,
+ "position": 2
+ },
+ "228": {
+ "code": 228,
+ "nom": "Immobilisations grev\u00e9es de droits",
+ "parent": 22,
+ "position": 2
+ },
+ "229": {
+ "code": 229,
+ "nom": "Droits des propri\u00e9taires",
+ "parent": 22,
+ "position": 2
+ },
+ "23": {
+ "code": 23,
+ "nom": "IMMOBILISATIONS EN COURS",
+ "parent": 2,
+ "position": 2
+ },
+ "231": {
+ "code": 231,
+ "nom": "Immobilisations corporelles en cours",
+ "parent": 23,
+ "position": 2
+ },
+ "238": {
+ "code": 238,
+ "nom": "Avances et acomptes vers\u00e9s sur commande d'immobilisations corporelles",
+ "parent": 23,
+ "position": 2
+ },
+ "26": {
+ "code": 26,
+ "nom": "PARTICIPATIONS ET CR\u00c9ANCES RATTACH\u00c9ES A DES PARTICIPATIONS",
+ "parent": 2,
+ "position": 2
+ },
+ "261": {
+ "code": 261,
+ "nom": "Titres de participation",
+ "parent": 26,
+ "position": 2
+ },
+ "27": {
+ "code": 27,
+ "nom": "AUTRES IMMOBILISATIONS FINANCI\u00c8RES",
+ "parent": 2,
+ "position": 2
+ },
+ "270": {
+ "code": 270,
+ "nom": "Participations financi\u00e8res",
+ "parent": 27,
+ "position": 2
+ },
+ "275": {
+ "code": 275,
+ "nom": "D\u00e9p\u00f4ts et cautionnements vers\u00e9s",
+ "parent": 27,
+ "position": 2
+ },
+ "28": {
+ "code": 28,
+ "nom": "AMORTISSEMENTS DES IMMOBILISATIONS",
+ "parent": 2,
+ "position": 2
+ },
+ "280": {
+ "code": 280,
+ "nom": "Amortissements des immobilisations incorporelles",
+ "parent": 28,
+ "position": 2
+ },
+ "281": {
+ "code": 281,
+ "nom": "Amortissements des immobilisations corporelles",
+ "parent": 28,
+ "position": 2
+ },
+ "29": {
+ "code": 29,
+ "nom": "D\u00c9PR\u00c9CIATION DES IMMOBILISATIONS",
+ "parent": 2,
+ "position": 2
+ },
+ "290": {
+ "code": 290,
+ "nom": "D\u00e9pr\u00e9ciation des immobilisations incorporelles",
+ "parent": 29,
+ "position": 2
+ },
+ "291": {
+ "code": 291,
+ "nom": "D\u00e9pr\u00e9ciation des immobilisations corporelles",
+ "parent": 29,
+ "position": 2
+ },
+ "3": {
+ "code": 3,
+ "nom": "Classe 3 \u2014 Comptes de stocks",
+ "parent": 0,
+ "position": 2
+ },
+ "31": {
+ "code": 31,
+ "nom": "MATIERES PREMIERES ET FOURNITURES",
+ "parent": 3,
+ "position": 2
+ },
+ "311": {
+ "code": 311,
+ "nom": "Mati\u00e8res",
+ "parent": 31,
+ "position": 2
+ },
+ "317": {
+ "code": 317,
+ "nom": "Fournitures",
+ "parent": 31,
+ "position": 2
+ },
+ "32": {
+ "code": 32,
+ "nom": "AUTRES APPROVISIONNEMENTS",
+ "parent": 3,
+ "position": 2
+ },
+ "321": {
+ "code": 321,
+ "nom": "Mati\u00e8res consommables",
+ "parent": 32,
+ "position": 2
+ },
+ "322": {
+ "code": 322,
+ "nom": "Fournitures consommables",
+ "parent": 32,
+ "position": 2
+ },
+ "33": {
+ "code": 33,
+ "nom": "EN-COURS DE PRODUCTION DE BIENS",
+ "parent": 3,
+ "position": 2
+ },
+ "331": {
+ "code": 331,
+ "nom": "Produits en cours",
+ "parent": 33,
+ "position": 2
+ },
+ "335": {
+ "code": 335,
+ "nom": "Travaux en cours",
+ "parent": 33,
+ "position": 2
+ },
+ "34": {
+ "code": 34,
+ "nom": "EN-COURS DE PRODUCTION DE SERVICES",
+ "parent": 3,
+ "position": 2
+ },
+ "35": {
+ "code": 35,
+ "nom": "STOCKS DE PRODUITS",
+ "parent": 3,
+ "position": 2
+ },
+ "351": {
+ "code": 351,
+ "nom": "Produits interm\u00e9diaires",
+ "parent": 35,
+ "position": 2
+ },
+ "355": {
+ "code": 355,
+ "nom": "Produits finis",
+ "parent": 35,
+ "position": 2
+ },
+ "358": {
+ "code": 358,
+ "nom": "Produits r\u00e9siduels",
+ "parent": 35,
+ "position": 2
+ },
+ "3581": {
+ "code": 3581,
+ "nom": "D\u00e9chets",
+ "parent": 358,
+ "position": 2
+ },
+ "3585": {
+ "code": 3585,
+ "nom": "Rebuts",
+ "parent": 358,
+ "position": 2
+ },
+ "3586": {
+ "code": 3586,
+ "nom": "Mati\u00e8re de r\u00e9cup\u00e9ration",
+ "parent": 358,
+ "position": 2
+ },
+ "37": {
+ "code": 37,
+ "nom": "STOCKS DE MARCHANDISES",
+ "parent": 3,
+ "position": 2
+ },
+ "370": {
+ "code": 370,
+ "nom": "Autres stocks de marchandises",
+ "parent": 37,
+ "position": 2
+ },
+ "39": {
+ "code": 39,
+ "nom": "PROVISIONS POUR DEPRECIATION DES STOCKS ET EN-COURS",
+ "parent": 3,
+ "position": 2
+ },
+ "391": {
+ "code": 391,
+ "nom": "Provisions pour d\u00e9pr\u00e9ciation des mati\u00e8res premi\u00e8res et fournitures",
+ "parent": 39,
+ "position": 2
+ },
+ "4": {
+ "code": 4,
+ "nom": "Classe 4 \u2014 Comptes de tiers",
+ "parent": 0,
+ "position": 3
+ },
+ "40": {
+ "code": 40,
+ "nom": "FOURNISSEURS ET COMPTES RATTACH\u00c9S",
+ "parent": 4,
+ "position": 1
+ },
+ "401": {
+ "code": 401,
+ "nom": "Fournisseurs",
+ "parent": 40,
+ "position": 1
+ },
+ "4010": {
+ "code": 4010,
+ "nom": "Autres fournisseurs",
+ "parent": 401,
+ "position": 1
+ },
+ "408": {
+ "code": 408,
+ "nom": "Fournisseurs - Factures non parvenues",
+ "parent": 40,
+ "position": 1
+ },
+ "409": {
+ "code": 409,
+ "nom": "Avances aux fournisseurs",
+ "parent": 40,
+ "position": 2
+ },
+ "41": {
+ "code": 41,
+ "nom": "USAGERS ET COMPTES RATTACH\u00c9S",
+ "parent": 4,
+ "position": 2
+ },
+ "411": {
+ "code": 411,
+ "nom": "Usagers",
+ "parent": 41,
+ "position": 2
+ },
+ "4110": {
+ "code": 4110,
+ "nom": "Autres usagers",
+ "parent": 411,
+ "position": 2
+ },
+ "419": {
+ "code": 419,
+ "nom": "Avances aux usagers",
+ "parent": 41,
+ "position": 1
+ },
+ "42": {
+ "code": 42,
+ "nom": "PERSONNEL ET COMPTES RATTACH\u00c9S",
+ "parent": 4,
+ "position": 1
+ },
+ "421": {
+ "code": 421,
+ "nom": "Personnel - R\u00e9mun\u00e9rations dues",
+ "parent": 42,
+ "position": 1
+ },
+ "4210": {
+ "code": 4210,
+ "nom": "Autres membres du personnel",
+ "parent": 421,
+ "position": 1
+ },
+ "425": {
+ "code": 425,
+ "nom": "Personnel - Avances et acomptes",
+ "parent": 42,
+ "position": 2
+ },
+ "428": {
+ "code": 428,
+ "nom": "Personnel - Charges \u00e0 payer et produits \u00e0 recevoir",
+ "parent": 42,
+ "position": 1
+ },
+ "43": {
+ "code": 43,
+ "nom": "S\u00c9CURIT\u00c9 SOCIALE ET AUTRES ORGANISMES SOCIAUX",
+ "parent": 4,
+ "position": 1
+ },
+ "430": {
+ "code": 430,
+ "nom": "Dettes et cr\u00e9dits envers les organismes sociaux",
+ "parent": 43,
+ "position": 1
+ },
+ "431": {
+ "code": 431,
+ "nom": "S\u00e9curit\u00e9 sociale",
+ "parent": 43,
+ "position": 1
+ },
+ "437": {
+ "code": 437,
+ "nom": "Autres organismes sociaux",
+ "parent": 43,
+ "position": 1
+ },
+ "4372": {
+ "code": 4372,
+ "nom": "Mutuelles",
+ "parent": 437,
+ "position": 1
+ },
+ "4373": {
+ "code": 4373,
+ "nom": "Caisse de retraite et de pr\u00e9voyance",
+ "parent": 437,
+ "position": 1
+ },
+ "4374": {
+ "code": 4374,
+ "nom": "Caisse d'allocations de ch\u00f4mage - P\u00f4le emploi",
+ "parent": 437,
+ "position": 1
+ },
+ "4375": {
+ "code": 4375,
+ "nom": "AGESSA",
+ "parent": 437,
+ "position": 1
+ },
+ "4378": {
+ "code": 4378,
+ "nom": "Autres organismes sociaux - Divers",
+ "parent": 437,
+ "position": 1
+ },
+ "438": {
+ "code": 438,
+ "nom": "Organismes sociaux - Charges \u00e0 payer et produits \u00e0 recevoir",
+ "parent": 43,
+ "position": 1
+ },
+ "4382": {
+ "code": 4382,
+ "nom": "Charges sociales sur cong\u00e9s \u00e0 payer",
+ "parent": 438,
+ "position": 1
+ },
+ "4386": {
+ "code": 4386,
+ "nom": "Autres charges \u00e0 payer",
+ "parent": 438,
+ "position": 1
+ },
+ "4387": {
+ "code": 4387,
+ "nom": "Produits \u00e0 recevoir",
+ "parent": 438,
+ "position": 2
+ },
+ "439": {
+ "code": 439,
+ "nom": "Avances aupr\u00e8s des organismes sociaux",
+ "parent": 43,
+ "position": 1
+ },
+ "44": {
+ "code": 44,
+ "nom": "\u00c9TAT ET AUTRES COLLECTIVIT\u00c9S PUBLIQUES",
+ "parent": 4,
+ "position": 2
+ },
+ "441": {
+ "code": 441,
+ "nom": "\u00c9tat - Subventions \u00e0 recevoir",
+ "parent": 44,
+ "position": 2
+ },
+ "4411": {
+ "code": 4411,
+ "nom": "Subventions d'investissement",
+ "parent": 441,
+ "position": 2
+ },
+ "4417": {
+ "code": 4417,
+ "nom": "Subventions d'exploitation",
+ "parent": 441,
+ "position": 2
+ },
+ "4418": {
+ "code": 4418,
+ "nom": "Subventions d'\u00e9quilibre",
+ "parent": 441,
+ "position": 2
+ },
+ "4419": {
+ "code": 4419,
+ "nom": "Avances sur subventions",
+ "parent": 441,
+ "position": 2
+ },
+ "442": {
+ "code": 442,
+ "nom": "\u00c9tat - Imp\u00f4ts et taxes recouvrables sur des tiers",
+ "parent": 44,
+ "position": 1
+ },
+ "444": {
+ "code": 444,
+ "nom": "\u00c9tat - Imp\u00f4ts sur les b\u00e9n\u00e9fices",
+ "parent": 44,
+ "position": 2
+ },
+ "445": {
+ "code": 445,
+ "nom": "\u00c9tat - Taxes sur le chiffre d'affaires",
+ "parent": 44,
+ "position": 2
+ },
+ "4455": {
+ "code": 4455,
+ "nom": "Taxes sur le chiffre d'affaires \u00e0 d\u00e9caisser",
+ "parent": 445,
+ "position": 2
+ },
+ "44551": {
+ "code": 44551,
+ "nom": "TVA \u00e0 d\u00e9caisser",
+ "parent": 4455,
+ "position": 2
+ },
+ "44558": {
+ "code": 44558,
+ "nom": "Taxes assimil\u00e9es \u00e0 la TVA",
+ "parent": 4455,
+ "position": 2
+ },
+ "4456": {
+ "code": 4456,
+ "nom": "Taxes sur le chiffre d'affaires d\u00e9ductibles",
+ "parent": 445,
+ "position": 2
+ },
+ "44562": {
+ "code": 44562,
+ "nom": "TVA sur immobilisations",
+ "parent": 4456,
+ "position": 2
+ },
+ "44566": {
+ "code": 44566,
+ "nom": "TVA sur autres biens et services",
+ "parent": 4456,
+ "position": 2
+ },
+ "4457": {
+ "code": 4457,
+ "nom": "Taxes sur le chiffre d'affaires collect\u00e9es par l'association",
+ "parent": 445,
+ "position": 2
+ },
+ "4458": {
+ "code": 4458,
+ "nom": "Taxes sur le chiffre d'affaires \u00e0 r\u00e9gulariser ou en attente",
+ "parent": 445,
+ "position": 2
+ },
+ "44581": {
+ "code": 44581,
+ "nom": "Acomptes - R\u00e9gime simplifi\u00e9 d'imposition",
+ "parent": 4458,
+ "position": 2
+ },
+ "44582": {
+ "code": 44582,
+ "nom": "Acomptes - R\u00e9gime du forfait",
+ "parent": 4458,
+ "position": 2
+ },
+ "44583": {
+ "code": 44583,
+ "nom": "Remboursement de taxes sur le chiffre d'affaires demand\u00e9",
+ "parent": 4458,
+ "position": 2
+ },
+ "44584": {
+ "code": 44584,
+ "nom": "TVA r\u00e9cup\u00e9r\u00e9e d'avance",
+ "parent": 4458,
+ "position": 2
+ },
+ "44586": {
+ "code": 44586,
+ "nom": "Taxes sur le chiffre d'affaires sur factures non parvenues",
+ "parent": 4458,
+ "position": 2
+ },
+ "44587": {
+ "code": 44587,
+ "nom": "Taxes sur le chiffre d'affaires sur factures \u00e0 \u00e9tablir",
+ "parent": 4458,
+ "position": 2
+ },
+ "447": {
+ "code": 447,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s",
+ "parent": 44,
+ "position": 1
+ },
+ "4471": {
+ "code": 4471,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s sur r\u00e9mun\u00e9rations (Administration des imp\u00f4ts)",
+ "parent": 447,
+ "position": 1
+ },
+ "44711": {
+ "code": 44711,
+ "nom": "Taxe sur les salaires",
+ "parent": 4471,
+ "position": 1
+ },
+ "44713": {
+ "code": 44713,
+ "nom": "Participation des employeurs \u00e0 la formation professionnelle continue",
+ "parent": 4471,
+ "position": 1
+ },
+ "44714": {
+ "code": 44714,
+ "nom": "Cotisation par d\u00e9faut d'investissement obligatoire dans la construction",
+ "parent": 4471,
+ "position": 1
+ },
+ "44718": {
+ "code": 44718,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s",
+ "parent": 4471,
+ "position": 1
+ },
+ "4473": {
+ "code": 4473,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s sur r\u00e9mun\u00e9rations (Autres organismes)",
+ "parent": 447,
+ "position": 1
+ },
+ "44733": {
+ "code": 44733,
+ "nom": "Participation des employeurs \u00e0 la formation professionnelle continue",
+ "parent": 4473,
+ "position": 1
+ },
+ "44734": {
+ "code": 44734,
+ "nom": "Participation des employeurs \u00e0 l'effort de construction (versements \u00e0 fonds perdus)",
+ "parent": 4473,
+ "position": 1
+ },
+ "4475": {
+ "code": 4475,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Administration des imp\u00f4ts)",
+ "parent": 447,
+ "position": 1
+ },
+ "4477": {
+ "code": 4477,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Autres organismes)",
+ "parent": 447,
+ "position": 1
+ },
+ "448": {
+ "code": 448,
+ "nom": "\u00c9tat - Charges \u00e0 payer et produits \u00e0 recevoir",
+ "parent": 44,
+ "position": 1
+ },
+ "4482": {
+ "code": 4482,
+ "nom": "Charges fiscales sur cong\u00e9s \u00e0 payer",
+ "parent": 448,
+ "position": 1
+ },
+ "4486": {
+ "code": 4486,
+ "nom": "Autres charges \u00e0 payer",
+ "parent": 448,
+ "position": 1
+ },
+ "4487": {
+ "code": 4487,
+ "nom": "Produits \u00e0 recevoir",
+ "parent": 448,
+ "position": 2
+ },
+ "449": {
+ "code": 449,
+ "nom": "Avances aupr\u00e8s de l'\u00e9tat et des collectivit\u00e9s publiques",
+ "parent": 44,
+ "position": 1
+ },
+ "45": {
+ "code": 45,
+ "nom": "CONF\u00c9D\u00c9RATION, F\u00c9D\u00c9RATION, UNIONS ET ASSOCIATIONS AFFILI\u00c9ES",
+ "parent": 4,
+ "position": 3
+ },
+ "451": {
+ "code": 451,
+ "nom": "Conf\u00e9d\u00e9ration, f\u00e9d\u00e9ration et associations affili\u00e9es - Compte courant",
+ "parent": 45,
+ "position": 3
+ },
+ "455": {
+ "code": 455,
+ "nom": "Soci\u00e9taires - Comptes courants",
+ "parent": 45,
+ "position": 3
+ },
+ "46": {
+ "code": 46,
+ "nom": "D\u00c9BITEURS DIVERS ET CR\u00c9DITEURS DIVERS",
+ "parent": 4,
+ "position": 3
+ },
+ "467": {
+ "code": 467,
+ "nom": "Autres comptes d\u00e9biteurs et cr\u00e9diteurs",
+ "parent": 46,
+ "position": 3
+ },
+ "468": {
+ "code": 468,
+ "nom": "Divers - Charges \u00e0 payer et produits \u00e0 recevoir",
+ "parent": 46,
+ "position": 3
+ },
+ "4686": {
+ "code": 4686,
+ "nom": "Charges \u00e0 payer",
+ "parent": 468,
+ "position": 1
+ },
+ "4687": {
+ "code": 4687,
+ "nom": "Produits \u00e0 recevoir",
+ "parent": 468,
+ "position": 2
+ },
+ "47": {
+ "code": 47,
+ "nom": "COMPTES TRANSITOIRES OU D'ATTENTE",
+ "parent": 4,
+ "position": 3
+ },
+ "471": {
+ "code": 471,
+ "nom": "Recettes \u00e0 classer",
+ "parent": 47,
+ "position": 1
+ },
+ "472": {
+ "code": 472,
+ "nom": "D\u00e9penses \u00e0 classer et \u00e0 r\u00e9gulariser",
+ "parent": 47,
+ "position": 2
+ },
+ "48": {
+ "code": 48,
+ "nom": "COMPTES DE R\u00c9GULARISATION",
+ "parent": 4,
+ "position": 3
+ },
+ "481": {
+ "code": 481,
+ "nom": "Charges \u00e0 r\u00e9partir sur plusieurs exercices",
+ "parent": 48,
+ "position": 2
+ },
+ "486": {
+ "code": 486,
+ "nom": "Charges constat\u00e9es d'avance",
+ "parent": 48,
+ "position": 2
+ },
+ "487": {
+ "code": 487,
+ "nom": "Produits constat\u00e9s d'avance",
+ "parent": 48,
+ "position": 1
+ },
+ "49": {
+ "code": 49,
+ "nom": "DEPRECIATION DES COMPTES DE TIERS",
+ "parent": 4,
+ "position": 2
+ },
+ "491": {
+ "code": 491,
+ "nom": "D\u00e9pr\u00e9ciation des comptes clients",
+ "parent": 49,
+ "position": 2
+ },
+ "496": {
+ "code": 496,
+ "nom": "D\u00e9pr\u00e9ciation des comptes d\u00e9biteurs divers",
+ "parent": 49,
+ "position": 2
+ },
+ "5": {
+ "code": 5,
+ "nom": "Classe 5 \u2014 Comptes financiers",
+ "parent": 0,
+ "position": 2
+ },
+ "50": {
+ "code": 50,
+ "nom": "VALEURS MOBILI\u00c8RES DE PLACEMENT",
+ "parent": 5,
+ "position": 2
+ },
+ "51": {
+ "code": 51,
+ "nom": "BANQUES, \u00c9TABLISSEMENTS FINANCIERS ET ASSIMIL\u00c9S",
+ "parent": 5,
+ "position": 2
+ },
+ "512": {
+ "code": 512,
+ "nom": "Banques",
+ "parent": 51,
+ "position": 2
+ },
+ "53": {
+ "code": 53,
+ "nom": "CAISSE",
+ "parent": 5,
+ "position": 2
+ },
+ "530": {
+ "code": 530,
+ "nom": "Caisse",
+ "parent": 53,
+ "position": 2
+ },
+ "54": {
+ "code": 54,
+ "nom": "R\u00c9GIES D'AVANCES ET ACCR\u00c9DITIFS",
+ "parent": 5,
+ "position": 2
+ },
+ "58": {
+ "code": 58,
+ "nom": "VIREMENTS INTERNES",
+ "parent": 5,
+ "position": 2
+ },
+ "59": {
+ "code": 59,
+ "nom": "PROVISIONS POUR D\u00c9PR\u00c9CIATION DES COMPTES FINANCIERS",
+ "parent": 5,
+ "position": 2
+ },
+ "6": {
+ "code": 6,
+ "nom": "Classe 6 \u2014 Comptes de charges",
+ "parent": 0,
+ "position": 8
+ },
+ "60": {
+ "code": 60,
+ "nom": "ACHATS",
+ "parent": 6,
+ "position": 8
+ },
+ "601": {
+ "code": 601,
+ "nom": "Achats stock\u00e9s - Mati\u00e8res premi\u00e8res et fournitures",
+ "parent": 60,
+ "position": 8
+ },
+ "602": {
+ "code": 602,
+ "nom": "Achats stock\u00e9s - Autres approvisionnements",
+ "parent": 60,
+ "position": 8
+ },
+ "604": {
+ "code": 604,
+ "nom": "Achat d'\u00e9tudes et prestations de services",
+ "parent": 60,
+ "position": 8
+ },
+ "606": {
+ "code": 606,
+ "nom": "Achats non stock\u00e9s de mati\u00e8res et fournitures",
+ "parent": 60,
+ "position": 8
+ },
+ "6061": {
+ "code": 6061,
+ "nom": "Fournitures non stockables (eau, \u00e9nergie...)",
+ "parent": 606,
+ "position": 8
+ },
+ "6063": {
+ "code": 6063,
+ "nom": "Fournitures d'entretien et de petit \u00e9quipement",
+ "parent": 606,
+ "position": 8
+ },
+ "6064": {
+ "code": 6064,
+ "nom": "Fournitures administratives",
+ "parent": 606,
+ "position": 8
+ },
+ "6068": {
+ "code": 6068,
+ "nom": "Autres mati\u00e8res et fournitures",
+ "parent": 606,
+ "position": 8
+ },
+ "607": {
+ "code": 607,
+ "nom": "Achats de marchandises",
+ "parent": 60,
+ "position": 8
+ },
+ "61": {
+ "code": 61,
+ "nom": "SERVICES EXT\u00c9RIEURS",
+ "parent": 6,
+ "position": 8
+ },
+ "611": {
+ "code": 611,
+ "nom": "Sous-traitance g\u00e9n\u00e9rale",
+ "parent": 61,
+ "position": 8
+ },
+ "612": {
+ "code": 612,
+ "nom": "Redevances de cr\u00e9dit-bail",
+ "parent": 61,
+ "position": 8
+ },
+ "613": {
+ "code": 613,
+ "nom": "Locations",
+ "parent": 61,
+ "position": 8
+ },
+ "614": {
+ "code": 614,
+ "nom": "Charges locatives et de co-propri\u00e9t\u00e9",
+ "parent": 61,
+ "position": 8
+ },
+ "615": {
+ "code": 615,
+ "nom": "Entretiens et r\u00e9parations",
+ "parent": 61,
+ "position": 8
+ },
+ "616": {
+ "code": 616,
+ "nom": "Primes d'assurance",
+ "parent": 61,
+ "position": 8
+ },
+ "618": {
+ "code": 618,
+ "nom": "Divers",
+ "parent": 61,
+ "position": 8
+ },
+ "62": {
+ "code": 62,
+ "nom": "AUTRES SERVICES EXT\u00c9RIEURS",
+ "parent": 6,
+ "position": 8
+ },
+ "621": {
+ "code": 621,
+ "nom": "Personnel ext\u00e9rieur \u00e0 l'association",
+ "parent": 62,
+ "position": 8
+ },
+ "622": {
+ "code": 622,
+ "nom": "R\u00e9mun\u00e9rations d'interm\u00e9diaires et honoraires",
+ "parent": 62,
+ "position": 8
+ },
+ "6226": {
+ "code": 6226,
+ "nom": "Honoraires",
+ "parent": 622,
+ "position": 8
+ },
+ "6227": {
+ "code": 6227,
+ "nom": "Frais d'actes et de contentieux",
+ "parent": 622,
+ "position": 8
+ },
+ "6228": {
+ "code": 6228,
+ "nom": "Divers",
+ "parent": 622,
+ "position": 8
+ },
+ "623": {
+ "code": 623,
+ "nom": "Publicit\u00e9, publications, relations publiques",
+ "parent": 62,
+ "position": 8
+ },
+ "624": {
+ "code": 624,
+ "nom": "Transports de biens et transports collectifs du personnel",
+ "parent": 62,
+ "position": 8
+ },
+ "625": {
+ "code": 625,
+ "nom": "D\u00e9placements, missions et r\u00e9ceptions",
+ "parent": 62,
+ "position": 8
+ },
+ "626": {
+ "code": 626,
+ "nom": "Frais postaux et de t\u00e9l\u00e9communications",
+ "parent": 62,
+ "position": 8
+ },
+ "627": {
+ "code": 627,
+ "nom": "Services bancaires et assimil\u00e9s",
+ "parent": 62,
+ "position": 8
+ },
+ "628": {
+ "code": 628,
+ "nom": "Divers",
+ "parent": 62,
+ "position": 8
+ },
+ "63": {
+ "code": 63,
+ "nom": "IMP\u00d4TS, TAXES ET VERSEMENTS ASSIMIL\u00c9S",
+ "parent": 6,
+ "position": 8
+ },
+ "631": {
+ "code": 631,
+ "nom": "Imp\u00f4ts, taxes et versements assimil\u00e9s sur r\u00e9mun\u00e9rations (Administration des imp\u00f4ts)",
+ "parent": 63,
+ "position": 8
+ },
+ "6311": {
+ "code": 6311,
+ "nom": "Taxes sur les salaires",
+ "parent": 631,
+ "position": 8
+ },
+ "6313": {
+ "code": 6313,
+ "nom": "Participations des employeurs \u00e0 la formation professionnelle continue",
+ "parent": 631,
+ "position": 8
+ },
+ "635": {
+ "code": 635,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Administration des imp\u00f4ts)",
+ "parent": 63,
+ "position": 8
+ },
+ "6351": {
+ "code": 6351,
+ "nom": "Imp\u00f4ts directs (sauf imp\u00f4ts sur les b\u00e9n\u00e9fices)",
+ "parent": 635,
+ "position": 8
+ },
+ "6353": {
+ "code": 6353,
+ "nom": "Imp\u00f4ts indirects",
+ "parent": 635,
+ "position": 8
+ },
+ "637": {
+ "code": 637,
+ "nom": "Autres imp\u00f4ts, taxes et versements assimil\u00e9s (Autres organismes)",
+ "parent": 63,
+ "position": 8
+ },
+ "64": {
+ "code": 64,
+ "nom": "CHARGES DE PERSONNEL",
+ "parent": 6,
+ "position": 8
+ },
+ "641": {
+ "code": 641,
+ "nom": "R\u00e9mun\u00e9rations du personnel",
+ "parent": 64,
+ "position": 8
+ },
+ "643": {
+ "code": 643,
+ "nom": "R\u00e9mun\u00e9rations du personnel artistique et assimil\u00e9s",
+ "parent": 64,
+ "position": 8
+ },
+ "645": {
+ "code": 645,
+ "nom": "Charges de s\u00e9curit\u00e9 sociale et de pr\u00e9voyance",
+ "parent": 64,
+ "position": 8
+ },
+ "647": {
+ "code": 647,
+ "nom": "Autres charges sociales",
+ "parent": 64,
+ "position": 8
+ },
+ "648": {
+ "code": 648,
+ "nom": "Autres charges de personnel",
+ "parent": 64,
+ "position": 8
+ },
+ "65": {
+ "code": 65,
+ "nom": "AUTRES CHARGES DE GESTION COURANTE",
+ "parent": 6,
+ "position": 8
+ },
+ "658": {
+ "code": 658,
+ "nom": "Charges diverses de gestion courante",
+ "parent": 65,
+ "position": 8
+ },
+ "66": {
+ "code": 66,
+ "nom": "CHARGES FINANCI\u00c8RES",
+ "parent": 6,
+ "position": 8
+ },
+ "661": {
+ "code": 661,
+ "nom": "Charges d'int\u00e9r\u00eats",
+ "parent": 66,
+ "position": 8
+ },
+ "67": {
+ "code": 67,
+ "nom": "CHARGES EXCEPTIONNELLES",
+ "parent": 6,
+ "position": 8
+ },
+ "671": {
+ "code": 671,
+ "nom": "Charges exceptionnelles sur op\u00e9rations de gestion",
+ "parent": 67,
+ "position": 8
+ },
+ "6713": {
+ "code": 6713,
+ "nom": "Dons, lib\u00e9ralit\u00e9s",
+ "parent": 671,
+ "position": 8
+ },
+ "678": {
+ "code": 678,
+ "nom": "Autres charges exceptionnelles",
+ "parent": 67,
+ "position": 8
+ },
+ "6788": {
+ "code": 6788,
+ "nom": "Charges exceptionnelles diverses",
+ "parent": 678,
+ "position": 8
+ },
+ "68": {
+ "code": 68,
+ "nom": "DOTATIONS AUX AMORTISSEMENTS, D\u00c9PR\u00c9CIATIONS, PROVISIONS ET ENGAGEMENTS",
+ "parent": 6,
+ "position": 8
+ },
+ "681": {
+ "code": 681,
+ "nom": "Dotations aux amortissements, d\u00e9pr\u00e9ciations et provisions - Charges d'exploitation",
+ "parent": 68,
+ "position": 8
+ },
+ "6811": {
+ "code": 6811,
+ "nom": "Dotations aux amortissements des immobilisations incorporelles et corporelles",
+ "parent": 681,
+ "position": 8
+ },
+ "68111": {
+ "code": 68111,
+ "nom": "Immobilisations incorporelles",
+ "parent": 6811,
+ "position": 8
+ },
+ "68112": {
+ "code": 68112,
+ "nom": "Immobilisations corporelles",
+ "parent": 6811,
+ "position": 8
+ },
+ "686": {
+ "code": 686,
+ "nom": "Dotations aux amortissements, d\u00e9pr\u00e9ciations et provisions - Charges financi\u00e8res",
+ "parent": 68,
+ "position": 8
+ },
+ "69": {
+ "code": 69,
+ "nom": "PARTICIPATION DES SALARI\u00c9S - IMP\u00d4TS SUR LES B\u00c9N\u00c9FICES ET ASSIMIL\u00c9S",
+ "parent": 6,
+ "position": 8
+ },
+ "695": {
+ "code": 695,
+ "nom": "Imp\u00f4ts sur les soci\u00e9t\u00e9s (y compris imp\u00f4ts sur les soci\u00e9t\u00e9s des personnes morales non lucratives)",
+ "parent": 69,
+ "position": 8
+ },
+ "7": {
+ "code": 7,
+ "nom": "Classe 7 \u2014 Comptes de produits",
+ "parent": 0,
+ "position": 4
+ },
+ "70": {
+ "code": 70,
+ "nom": "VENTES DE PRODUITS FINIS, PRESTATIONS DE SERVICES, MARCHANDISES",
+ "parent": 7,
+ "position": 4
+ },
+ "701": {
+ "code": 701,
+ "nom": "Ventes de produits finis",
+ "parent": 70,
+ "position": 4
+ },
+ "706": {
+ "code": 706,
+ "nom": "Prestations de services",
+ "parent": 70,
+ "position": 4
+ },
+ "707": {
+ "code": 707,
+ "nom": "Ventes de marchandises",
+ "parent": 70,
+ "position": 4
+ },
+ "708": {
+ "code": 708,
+ "nom": "Produits des activit\u00e9s annexes",
+ "parent": 70,
+ "position": 4
+ },
+ "71": {
+ "code": 71,
+ "nom": "PRODUCTION STOCK\u00c9E (OU D\u00c9STOCKAGE)",
+ "parent": 7,
+ "position": 4
+ },
+ "72": {
+ "code": 72,
+ "nom": "PRODUCTION IMMOBILIS\u00c9E",
+ "parent": 7,
+ "position": 4
+ },
+ "74": {
+ "code": 74,
+ "nom": "SUBVENTIONS D'EXPLOITATION",
+ "parent": 7,
+ "position": 4
+ },
+ "740": {
+ "code": 740,
+ "nom": "Subventions re\u00e7ues",
+ "parent": 74,
+ "position": 4
+ },
+ "75": {
+ "code": 75,
+ "nom": "AUTRES PRODUITS DE GESTION COURANTE",
+ "parent": 7,
+ "position": 4
+ },
+ "754": {
+ "code": 754,
+ "nom": "Collectes",
+ "parent": 75,
+ "position": 4
+ },
+ "756": {
+ "code": 756,
+ "nom": "Cotisations",
+ "parent": 75,
+ "position": 4
+ },
+ "758": {
+ "code": 758,
+ "nom": "Produits divers de gestion courante",
+ "parent": 75,
+ "position": 4
+ },
+ "7587": {
+ "code": 7587,
+ "nom": "Ventes de dons en nature",
+ "parent": 758,
+ "position": 4
+ },
+ "7588": {
+ "code": 7588,
+ "nom": "Autres produits de la g\u00e9n\u00e9rosit\u00e9 du public",
+ "parent": 758,
+ "position": 4
+ },
+ "76": {
+ "code": 76,
+ "nom": "PRODUITS FINANCIERS",
+ "parent": 7,
+ "position": 4
+ },
+ "760": {
+ "code": 760,
+ "nom": "Produits financiers",
+ "parent": 76,
+ "position": 4
+ },
+ "77": {
+ "code": 77,
+ "nom": "PRODUITS EXCEPTIONNELS",
+ "parent": 7,
+ "position": 4
+ },
+ "771": {
+ "code": 771,
+ "nom": "Produits exceptionnels sur op\u00e9rations de gestion",
+ "parent": 77,
+ "position": 4
+ },
+ "7713": {
+ "code": 7713,
+ "nom": "Lib\u00e9ralit\u00e9s re\u00e7ues",
+ "parent": 771,
+ "position": 4
+ },
+ "7715": {
+ "code": 7715,
+ "nom": "Subventions d'\u00e9quilibre",
+ "parent": 771,
+ "position": 4
+ },
+ "775": {
+ "code": 775,
+ "nom": "Produits des cessions d'\u00e9l\u00e9ments d'actifs",
+ "parent": 77,
+ "position": 4
+ },
+ "778": {
+ "code": 778,
+ "nom": "Autres produits exceptionnels",
+ "parent": 77,
+ "position": 4
+ },
+ "7780": {
+ "code": 7780,
+ "nom": "Manifestations diverses",
+ "parent": 778,
+ "position": 4
+ },
+ "7788": {
+ "code": 7788,
+ "nom": "Produits exceptionnels divers",
+ "parent": 778,
+ "position": 4
+ },
+ "78": {
+ "code": 78,
+ "nom": "REPRISES SUR AMORTISSEMENTS ET PROVISIONS",
+ "parent": 7,
+ "position": 4
+ },
+ "79": {
+ "code": 79,
+ "nom": "TRANSFERT DE CHARGES",
+ "parent": 7,
+ "position": 4
+ },
+ "791": {
+ "code": 791,
+ "nom": "Transferts de charges d'exploitation",
+ "parent": 79,
+ "position": 4
+ },
+ "796": {
+ "code": 796,
+ "nom": "Transferts de charges financi\u00e8res",
+ "parent": 79,
+ "position": 4
+ },
+ "797": {
+ "code": 797,
+ "nom": "Transferts de charges exceptionnels",
+ "parent": 79,
+ "position": 4
+ },
+ "8": {
+ "code": 8,
+ "nom": "Classe 8 \u00ad\u2014 Contributions b\u00e9n\u00e9voles en nature",
+ "parent": 0,
+ "position": 12
+ },
+ "86": {
+ "code": 86,
+ "nom": "R\u00c9PARTITION PAR NATURE DE CHARGES",
+ "parent": 8,
+ "position": 8
+ },
+ "861": {
+ "code": 861,
+ "nom": "Mise \u00e0 dispositions gratuites de biens",
+ "parent": 86,
+ "position": 8
+ },
+ "862": {
+ "code": 862,
+ "nom": "Prestations",
+ "parent": 86,
+ "position": 8
+ },
+ "864": {
+ "code": 864,
+ "nom": "Personnel b\u00e9n\u00e9vole",
+ "parent": 86,
+ "position": 8
+ },
+ "87": {
+ "code": 87,
+ "nom": "R\u00c9PARTITION PAR NATURE DE RESSOURCES",
+ "parent": 8,
+ "position": 4
+ },
+ "870": {
+ "code": 870,
+ "nom": "B\u00e9n\u00e9volat",
+ "parent": 87,
+ "position": 4
+ },
+ "871": {
+ "code": 871,
+ "nom": "Prestations en nature",
+ "parent": 87,
+ "position": 4
+ },
+ "875": {
+ "code": 875,
+ "nom": "Dons en nature",
+ "parent": 87,
+ "position": 4
+ },
+ "9": {
+ "code": 9,
+ "nom": "Classe 9 \u2014 Comptes analytiques",
+ "parent": 0,
+ "position": 12
+ }
+}
\ No newline at end of file
diff --git a/include/data/schema.sql b/include/data/schema.sql
new file mode 100644
index 0000000..2cc846f
--- /dev/null
+++ b/include/data/schema.sql
@@ -0,0 +1,316 @@
+CREATE TABLE config (
+-- Configuration de Garradin
+ cle TEXT PRIMARY KEY,
+ valeur TEXT
+);
+
+-- On stocke ici les ID de catégorie de compta correspondant aux types spéciaux
+-- compta_categorie_cotisations => id_categorie
+-- compta_categorie_dons => id_categorie
+
+CREATE TABLE membres_categories
+-- Catégories de membres
+(
+ id INTEGER PRIMARY KEY,
+ nom TEXT,
+ description TEXT,
+
+ droit_wiki INT DEFAULT 1,
+ droit_membres INT DEFAULT 1,
+ droit_compta INT DEFAULT 1,
+ droit_inscription INT DEFAULT 0,
+ droit_connexion INT DEFAULT 1,
+ droit_config INT DEFAULT 0,
+ cacher INT DEFAULT 0,
+
+ id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id)
+);
+
+-- Membres de l'asso
+-- Table dynamique générée par l'application
+-- voir class.champs_membres.php
+
+CREATE TABLE cotisations
+-- Types de cotisations et activités
+(
+ id INTEGER PRIMARY KEY,
+ id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta
+
+ intitule TEXT NOT NULL,
+ description TEXT NULL,
+ montant REAL NOT NULL,
+
+ duree INTEGER NULL, -- En jours
+ debut TEXT NULL, -- timestamp
+ fin TEXT NULL,
+
+ FOREIGN KEY (id_categorie_compta) REFERENCES compta_categories (id)
+);
+
+CREATE TABLE cotisations_membres
+-- Enregistrement des cotisations et activités
+(
+ id INTEGER NOT NULL PRIMARY KEY,
+ id_membre INTEGER NOT NULL REFERENCES membres (id),
+ id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
+
+ date TEXT NOT NULL DEFAULT CURRENT_DATE
+);
+
+CREATE UNIQUE INDEX cm_unique ON cotisations_membres (id_membre, id_cotisation, date);
+
+CREATE TABLE membres_operations
+-- Liaision des enregistrement des paiements en compta
+(
+ id_membre INTEGER NOT NULL REFERENCES membres (id),
+ id_operation INTEGER NOT NULL REFERENCES compta_journal (id),
+ id_cotisation INTEGER NULL REFERENCES cotisations_membres (id),
+
+ PRIMARY KEY (id_membre, id_operation)
+);
+
+CREATE TABLE rappels
+-- Rappels de devoir renouveller une cotisation
+(
+ id INTEGER PRIMARY KEY,
+ id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
+
+ delai INTEGER NOT NULL, -- Délai en jours pour envoyer le rappel
+
+ sujet TEXT NOT NULL,
+ texte TEXT NOT NULL
+);
+
+CREATE TABLE rappels_envoyes
+-- Enregistrement des rappels envoyés à qui et quand
+(
+ id INTEGER PRIMARY KEY,
+
+ id_membre INTEGER NOT NULL REFERENCES membres (id),
+ id_cotisation INTEGER NOT NULL REFERENCES cotisations (id),
+
+ date TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ media INTEGER NOT NULL -- Média utilisé pour le rappel : 1 = email, 2 = courrier, 3 = autre
+);
+
+--
+-- WIKI
+--
+
+CREATE TABLE wiki_pages
+-- Pages du wiki
+(
+ id INTEGER PRIMARY KEY,
+ uri TEXT, -- URI unique (équivalent NomPageWiki)
+ titre TEXT,
+ date_creation TEXT DEFAULT CURRENT_TIMESTAMP,
+ date_modification TEXT DEFAULT CURRENT_TIMESTAMP,
+ parent INTEGER DEFAULT 0, -- ID de la page parent
+ revision INTEGER DEFAULT 0, -- Numéro de révision (commence à 0 si pas de texte, +1 à chaque changement du texte)
+ droit_lecture INTEGER DEFAULT 0, -- Accès en lecture (-1 = public [site web], 0 = tous ceux qui ont accès en lecture au wiki, 1+ = ID de groupe)
+ droit_ecriture INTEGER DEFAULT 0 -- Accès en écriture (0 = tous ceux qui ont droit d'écriture sur le wiki, 1+ = ID de groupe)
+);
+
+CREATE UNIQUE INDEX wiki_uri ON wiki_pages (uri);
+
+CREATE VIRTUAL TABLE wiki_recherche USING fts4
+-- Table dupliquée pour chercher une page
+(
+ id INT PRIMARY KEY NOT NULL, -- Clé externe obligatoire
+ titre TEXT,
+ contenu TEXT, -- Contenu de la dernière révision
+ FOREIGN KEY (id) REFERENCES wiki_pages(id)
+);
+
+CREATE TABLE wiki_revisions
+-- Révisions du contenu des pages
+(
+ id_page INTEGER NOT NULL,
+ revision INTEGER,
+
+ id_auteur INTEGER,
+
+ contenu TEXT,
+ modification TEXT, -- Description des modifications effectuées
+ chiffrement INTEGER DEFAULT 0, -- 1 si le contenu est chiffré, 0 sinon
+ date TEXT DEFAULT CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(id_page, revision),
+ FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire
+ FOREIGN KEY (id_auteur) REFERENCES membres (id) -- Clé externe non-obligatoire (peut être supprimée après en cas de suppression de membre)
+);
+
+CREATE INDEX wiki_revisions_id_page ON wiki_revisions (id_page);
+CREATE INDEX wiki_revisions_id_auteur ON wiki_revisions (id_auteur);
+
+-- Triggers pour synchro avec table wiki_pages
+CREATE TRIGGER wiki_recherche_delete AFTER DELETE ON wiki_pages
+ BEGIN
+ DELETE FROM wiki_recherche WHERE id = old.id;
+ END;
+
+CREATE TRIGGER wiki_recherche_update AFTER UPDATE OF id, titre ON wiki_pages
+ BEGIN
+ UPDATE wiki_recherche SET id = new.id, titre = new.titre WHERE id = old.id;
+ END;
+
+-- Trigger pour mettre à jour le contenu de la table de recherche lors d'une nouvelle révision
+CREATE TRIGGER wiki_recherche_contenu_insert AFTER INSERT ON wiki_revisions WHEN new.chiffrement != 1
+ BEGIN
+ UPDATE wiki_recherche SET contenu = new.contenu WHERE id = new.id_page;
+ END;
+
+-- Si le contenu est chiffré, la recherche n'affiche pas de contenu
+CREATE TRIGGER wiki_recherche_contenu_chiffre AFTER INSERT ON wiki_revisions WHEN new.chiffrement = 1
+ BEGIN
+ UPDATE wiki_recherche SET contenu = '' WHERE id = new.id_page;
+ END;
+
+/*
+CREATE TABLE wiki_fichiers (
+ id INTEGER PRIMARY KEY,
+ id_page INTEGER NOT NULL,
+ nom TEXT,
+ hash TEXT,
+
+ FOREIGN KEY (id_page) REFERENCES wiki_pages (id) -- Clé externe obligatoire
+);
+
+CREATE INDEX wiki_fichiers_id_page ON wiki_fichiers (id_page);
+
+CREATE TABLE wiki_suivi
+-- Suivi des pages
+(
+ id_membre INTEGER NOT NULL,
+ id_page INTEGER NOT NULL,
+
+ PRIMARY KEY (id_membre, id_page),
+
+ FOREIGN KEY (id_page) REFERENCES wiki_pages (id), -- Clé externe obligatoire
+ FOREIGN KEY (id_membre) REFERENCES membres (id) -- Clé externe obligatoire
+);
+*/
+
+--
+-- COMPTA
+--
+
+CREATE TABLE compta_exercices
+-- Exercices
+(
+ id INTEGER PRIMARY KEY,
+
+ libelle TEXT NOT NULL,
+
+ debut TEXT NOT NULL DEFAULT CURRENT_DATE,
+ fin TEXT NULL DEFAULT NULL,
+
+ cloture INTEGER NOT NULL DEFAULT 0
+);
+
+
+CREATE TABLE compta_comptes
+-- Plan comptable
+(
+ id TEXT PRIMARY KEY, -- peut contenir des lettres, eg. 53A, 53B, etc.
+ parent TEXT NOT NULL DEFAULT 0,
+
+ libelle TEXT NOT NULL,
+
+ position INTEGER NOT NULL, -- position actif/passif/charge/produit
+ plan_comptable INTEGER NOT NULL DEFAULT 1, -- 1 = fait partie du plan comptable, 0 = a été ajouté par l'utilisateur
+ desactive INTEGER NOT NULL DEFAULT 0 -- 1 = compte historique désactivé
+);
+
+CREATE INDEX compta_comptes_parent ON compta_comptes (parent);
+
+CREATE TABLE compta_comptes_bancaires
+-- Comptes bancaires
+(
+ id TEXT PRIMARY KEY,
+
+ banque TEXT NOT NULL,
+
+ iban TEXT,
+ bic TEXT,
+
+ FOREIGN KEY(id) REFERENCES compta_comptes(id)
+);
+
+CREATE TABLE compta_journal
+-- Journal des opérations comptables
+(
+ id INTEGER PRIMARY KEY,
+
+ libelle TEXT NOT NULL,
+ remarques TEXT,
+ numero_piece TEXT, -- N° de pièce comptable
+
+ montant REAL,
+
+ date TEXT DEFAULT CURRENT_DATE,
+ moyen_paiement TEXT DEFAULT NULL,
+ numero_cheque TEXT DEFAULT NULL,
+
+ compte_debit TEXT, -- N° du compte dans le plan
+ compte_credit TEXT, -- N° du compte dans le plan
+
+ id_exercice INTEGER NULL DEFAULT NULL, -- En cas de compta simple, l'exercice est permanent (NULL)
+ id_auteur INTEGER NULL,
+ id_categorie INTEGER NULL, -- Numéro de catégorie (en mode simple)
+
+ FOREIGN KEY(moyen_paiement) REFERENCES compta_moyens_paiement(code),
+ FOREIGN KEY(compte_debit) REFERENCES compta_comptes(id),
+ FOREIGN KEY(compte_credit) REFERENCES compta_comptes(id),
+ FOREIGN KEY(id_exercice) REFERENCES compta_exercices(id),
+ FOREIGN KEY(id_auteur) REFERENCES membres(id),
+ FOREIGN KEY(id_categorie) REFERENCES compta_categories(id)
+);
+
+CREATE INDEX compta_operations_exercice ON compta_journal (id_exercice);
+CREATE INDEX compta_operations_date ON compta_journal (date);
+CREATE INDEX compta_operations_comptes ON compta_journal (compte_debit, compte_credit);
+CREATE INDEX compta_operations_auteur ON compta_journal (id_auteur);
+
+CREATE TABLE compta_moyens_paiement
+-- Moyens de paiement
+(
+ code TEXT PRIMARY KEY,
+ nom TEXT
+);
+
+--INSERT INTO compta_moyens_paiement (code, nom) VALUES ('AU', 'Autre');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CB', 'Carte bleue');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('CH', 'Chèque');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('ES', 'Espèces');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('PR', 'Prélèvement');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('TI', 'TIP');
+INSERT INTO compta_moyens_paiement (code, nom) VALUES ('VI', 'Virement');
+
+CREATE TABLE compta_categories
+-- Catégories pour simplifier le plan comptable
+(
+ id INTEGER PRIMARY KEY,
+ type INTEGER DEFAULT 1, -- 1 = recette, -1 = dépense, 0 = autre (utilisé uniquement pour l'interface)
+
+ intitule TEXT NOT NULL,
+ description TEXT,
+
+ compte TEXT NOT NULL, -- Compte affecté par cette catégorie
+
+ FOREIGN KEY(compte) REFERENCES compta_comptes(id)
+);
+
+CREATE TABLE plugins
+(
+ id TEXT PRIMARY KEY,
+ officiel INTEGER NOT NULL DEFAULT 0,
+ nom TEXT NOT NULL,
+ description TEXT,
+ auteur TEXT,
+ url TEXT,
+ version TEXT NOT NULL,
+ menu INTEGER NOT NULL DEFAULT 0,
+ config TEXT
+);
\ No newline at end of file
diff --git a/include/index.html b/include/index.html
new file mode 100644
index 0000000..9a31a28
--- /dev/null
+++ b/include/index.html
@@ -0,0 +1 @@
+404 Not Found Not Found The requested URL was not found on this server.
\ No newline at end of file
diff --git a/include/init.php b/include/init.php
new file mode 100644
index 0000000..fe4e820
--- /dev/null
+++ b/include/init.php
@@ -0,0 +1,353 @@
+
+ \__/ (xx) //||\\\\
+ Erreur fatale
+ Une erreur fatale s\'est produite à l\'exécution de Garradin. Pour rapporter ce bug
+ merci d\'inclure le message ci-dessous :
+ ');
+ ini_set('error_append_string', '
+ Comment rapporter un bug
');
+ }
+}
+
+/*
+ * Gestion des erreurs et exceptions
+ */
+
+class UserException extends \LogicException
+{
+}
+
+function exception_error_handler($errno, $errstr, $errfile, $errline )
+{
+ // For @ ignored errors
+ if (error_reporting() === 0) return;
+ throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
+}
+
+function exception_handler($e)
+{
+ if ($e instanceOf UserException || $e instanceOf miniSkelMarkupException)
+ {
+ try {
+ if (PHP_SAPI == 'cli')
+ {
+ echo $e->getMessage();
+ }
+ else
+ {
+ $tpl = Template::getInstance();
+
+ $tpl->assign('error', $e->getMessage());
+ $tpl->display('error.tpl');
+ }
+
+ exit;
+ }
+ catch (Exception $e)
+ {
+ }
+ }
+
+ $file = str_replace(ROOT, '', $e->getFile());
+
+ $error = "Exception of type ".get_class($e)." happened !\n\n".
+ $e->getCode()." - ".$e->getMessage()."\n\nIn: ".
+ $file . ":" . $e->getLine()."\n\n";
+
+ if (!empty($_SERVER['HTTP_HOST']) && !empty($_SERVER['REQUEST_URI']))
+ $error .= 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']."\n\n";
+
+ $error .= $e->getTraceAsString();
+ $error .= "\n-------------\n";
+ $error .= 'Garradin version: ' . garradin_version() . "\n";
+ $error .= 'Garradin manifest: ' . garradin_manifest() . "\n";
+ $error .= 'PHP version: ' . phpversion() . "\n";
+
+ foreach ($_SERVER as $key=>$value)
+ {
+ if (is_array($value))
+ $value = json_encode($value);
+
+ $error .= $key . ': ' . $value . "\n";
+ }
+
+ $error = str_replace("\r", '', $error);
+ error_log($error);
+
+ if (MAIL_ERRORS)
+ {
+ mail(MAIL_ERRORS, '[Garradin] Erreur d\'exécution', $error, 'From: "' . WWW_URL . '" ');
+ }
+
+ if (PHP_SAPI == 'cli')
+ {
+ echo $error;
+ }
+ else
+ {
+ echo '
+ \__/ (xx) //||\\\\
+ Erreur d\'exécution ';
+
+ if (SHOW_ERRORS)
+ {
+ echo 'Une erreur s\'est produite à l\'exécution de Garradin. Pour rapporter ce bug
+ merci d\'inclure le message suivant :
+
+
+ Comment rapporter un bug
';
+ }
+ else
+ {
+ echo 'Une erreur s\'est produite à l\'exécution de Garradin.
+ Le webmaster a été prévenu.
';
+ }
+ }
+
+ exit;
+}
+
+set_error_handler('Garradin\exception_error_handler');
+set_exception_handler('Garradin\exception_handler');
+
+/**
+ * Auto-load classes and libs
+ */
+class Loader
+{
+ /**
+ * Already loaded filenames
+ * @var array
+ */
+ static protected $loaded = [];
+
+ static protected $libs = [
+ 'utils',
+ 'squelette_filtres',
+ 'static_cache',
+ 'template'
+ ];
+
+ /**
+ * Loads a class from the $name
+ * @param stringg $classname
+ * @return bool true
+ */
+ static public function load($classname)
+ {
+ $classname = ltrim($classname, '\\');
+ $filename = '';
+ $namespace = '';
+
+ if ($lastnspos = strripos($classname, '\\'))
+ {
+ $namespace = substr($classname, 0, $lastnspos);
+ $classname = substr($classname, $lastnspos + 1);
+
+ if ($namespace != 'Garradin')
+ {
+ $filename = str_replace('\\', '/', $namespace) . '/';
+ }
+ }
+
+ $classname = strtolower($classname);
+
+ if (in_array($classname, self::$libs)) {
+ $filename = 'lib.' . $classname . '.php';
+ } else {
+ $filename .= 'class.' . $classname . '.php';
+ }
+
+ $filename = ROOT . '/include/' . $filename;
+
+ if (array_key_exists($filename, self::$loaded))
+ {
+ return true;
+ }
+
+ if (!file_exists($filename)) {
+ throw new \Exception('File '.$filename.' doesn\'t exists');
+ }
+
+ self::$loaded[$filename] = true;
+
+ require $filename;
+ }
+}
+
+\spl_autoload_register(['Garradin\Loader', 'load'], true);
+
+$n = new Membres;
+
+/*
+ * Inclusion des fichiers de base
+ */
+
+if (!defined('Garradin\INSTALL_PROCESS') && !defined('Garradin\UPGRADE_PROCESS'))
+{
+ if (!file_exists(DB_FILE))
+ {
+ utils::redirect('/admin/install.php');
+ }
+
+ $config = Config::getInstance();
+
+ if (version_compare($config->getVersion(), garradin_version(), '<'))
+ {
+ utils::redirect('/admin/upgrade.php');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/lib.squelette_filtres.php b/include/lib.squelette_filtres.php
new file mode 100644
index 0000000..aab766d
--- /dev/null
+++ b/include/lib.squelette_filtres.php
@@ -0,0 +1,350 @@
+ 'supprimer_tags',
+ 'var_dump',
+ ];
+
+ static public $filtres_alias = [
+ '!=' => 'different_de',
+ '==' => 'egal_a',
+ '?' => 'choixsivide',
+ '>' => 'superieur_a',
+ '>=' => 'superieur_ou_egal_a',
+ '<' => 'inferieur_a',
+ '<=' => 'inferieur_ou_egal_a',
+ 'yes' => 'oui',
+ 'no' => 'non',
+ 'and' => 'et',
+ 'or' => 'ou',
+ 'xor' => 'xou',
+ ];
+
+ static public $desactiver_defaut = [
+ 'formatter_texte',
+ 'entites_html',
+ 'proteger_contact',
+ 'echapper_xml',
+ ];
+
+ static public function date_en_francais($date)
+ {
+ return ucfirst(strtolower(utils::strftime_fr('%A %e %B %Y', $date)));
+ }
+
+ static public function heure_en_francais($date)
+ {
+ return utils::strftime_fr('%Hh%I', $date);
+ }
+
+ static public function mois_en_francais($date)
+ {
+ return utils::strftime_fr('%B %Y', $date);
+ }
+
+ static public function date_perso($date, $format)
+ {
+ return utils::strftime_fr($format, $date);
+ }
+
+ static public function date_intelligente($date)
+ {
+ if (date('Ymd', $date) == date('Ymd'))
+ return 'Aujourd\'hui, '.date('H\hi', $date);
+ elseif (date('Ymd', $date) == date('Ymd', strtotime('yesterday')))
+ return 'Hier, '.date('H\hi', $date);
+ elseif (date('Y', $date) == date('Y'))
+ return strtolower(utils::strftime_fr('%e %B, %Hh%M', $date));
+ else
+ return strtolower(utils::strftime_fr('%e %B %Y', $date));
+ }
+
+ static public function date_atom($date)
+ {
+ return date(DATE_ATOM, $date);
+ }
+
+ static public function alterner($v, $name, $valeur1, $valeur2)
+ {
+ if (!array_key_exists($name, self::$alt))
+ {
+ self::$alt[$name] = 0;
+ }
+
+ if (self::$alt[$name]++ % 2 == 0)
+ return $valeur1;
+ else
+ return $valeur2;
+ }
+
+ static public function proteger_contact($contact)
+ {
+ if (!trim($contact))
+ return '';
+
+ if (strpos($contact, '@'))
+ return ''.htmlspecialchars(strrev($contact), ENT_QUOTES, 'UTF-8').' ';
+ else
+ return ''.htmlspecialchars($contact, ENT_QUOTES, 'UTF-8').' ';
+ }
+
+ static public function entites_html($texte)
+ {
+ return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8');
+ }
+
+ static public function echapper_xml($texte)
+ {
+ return str_replace(''', ''', htmlspecialchars($texte, ENT_QUOTES, 'UTF-8'));
+ }
+
+ static public function formatter_texte($texte)
+ {
+ $texte = utils::htmlLinksOnUrls($texte);
+ $texte = utils::htmlSpip($texte);
+ $texte = utils::htmlGarbage2xhtml($texte);
+
+ $texte = self::typo_fr($texte);
+
+ return $texte;
+ }
+
+ static public function typo_fr($str, $html = true)
+ {
+ $space = $html ? ' ' : ' ';
+ $str = preg_replace('/(?:[\h]| )*([?!:»])(\s+|$)/u', $space.'\\1\\2', $str);
+ $str = preg_replace('/(^|\s+)([«])(?:[\h]| )*/u', '\\1\\2'.$space, $str);
+ return $str;
+ }
+
+ static public function pagination($total, $debut, $par_page)
+ {
+ $max_page = ceil($total / $par_page);
+ $current = ($debut > 0) ? ceil($debut / $par_page) + 1 : 1;
+ $out = '';
+
+ if ($current > 1)
+ {
+ $out .= '« Page précédente - ';
+ }
+
+ for ($i = 1; $i <= $max_page; $i++)
+ {
+ $link = ($i == 1) ? './' : './+' . (($i - 1) * $par_page);
+
+ if ($i == $current)
+ $out .= ''.$i.' - ';
+ else
+ $out .= ''.$i.' - ';
+ }
+
+ if ($current < $max_page)
+ {
+ $out .= 'Page suivante » ';
+ }
+ else
+ {
+ $out = substr($out, 0, -3);
+ }
+
+ return $out;
+ }
+
+ // Compatibilité SPIP
+
+ static public function egal_a($value, $test)
+ {
+ if ($value == $test)
+ return true;
+ else
+ return false;
+ }
+
+ static public function different_de($value, $test)
+ {
+ if ($value != $test)
+ return true;
+ else
+ return false;
+ }
+
+ // disponible aussi avec : | ?{sioui, sinon}
+ static public function choixsivide($value, $un, $deux = '')
+ {
+ if (empty($value) || !trim($value))
+ return $deux;
+ else
+ return $un;
+ }
+
+ static public function sinon($value, $sinon = '')
+ {
+ if ($value)
+ return $value;
+ else
+ return $sinon;
+ }
+
+ static public function choixsiegal($value, $test, $un, $deux)
+ {
+ return ($value == $test) ? $un : $deux;
+ }
+
+ static public function supprimer_tags($value, $replace = '')
+ {
+ return preg_replace('!<[^>]*>!', $replace, $value);
+ }
+
+ static public function supprimer_spip($value)
+ {
+ $value = preg_replace('!\[([^\]]+)(?:->[^\]]*)?\]!U', '$1', $value);
+ $value = preg_replace('!\{+([^\}]*)\}+!', '$1', $value);
+ return $value;
+ }
+
+ static public function couper($texte, $taille, $etc = ' (...)')
+ {
+ if (strlen($texte) > $taille)
+ {
+ $texte = substr($texte, 0, $taille);
+ $taille -= ($taille * 0.1);
+
+ $texte = preg_replace('!([\s.,;:\!?])[^\s.,;:\!?]*?$!', '\\1', $texte);
+ $texte.= $etc;
+ }
+
+ return $texte;
+ }
+
+ static public function replace($texte, $expression, $replace, $modif='UsimsS')
+ {
+ return preg_replace('/'.$expression.'/'.$modif, $replace, $texte);
+ }
+
+ static public function plus($a, $b)
+ {
+ return $a + $b;
+ }
+
+ static public function moins($a, $b)
+ {
+ return $a - $b;
+ }
+
+ static public function mult($a, $b)
+ {
+ return $a * $b;
+ }
+
+ static public function div($a, $b)
+ {
+ return $b ? $a / $b : 0;
+ }
+
+ static public function modulo($a, $mod, $add)
+ {
+ return ($mod ? $nb % $mod : 0) + $add;
+ }
+
+ static public function vide($value)
+ {
+ return '';
+ }
+
+ static public function concat()
+ {
+ return implode('', func_get_args());
+ }
+
+ static public function singulier_ou_pluriel($nb, $singulier, $pluriel, $var = null)
+ {
+ if (!$nb)
+ return '';
+
+ if ($nb == 1)
+ return str_replace('@'.$var.'@', $nb, $singulier);
+ else
+ return str_replace('@'.$var.'@', $nb, $pluriel);
+ }
+
+ static public function date_w3c($date)
+ {
+ return date(DATE_W3C, $date);
+ }
+
+ static public function et($value, $test)
+ {
+ return ($value && $test);
+ }
+
+ static public function ou($value, $test)
+ {
+ return ($value || $test);
+ }
+
+ static public function xou($value, $test)
+ {
+ return ($value XOR $test);
+ }
+
+ static public function oui($value)
+ {
+ return $value ? true : false;
+ }
+
+ static public function non($value)
+ {
+ return !$value ? true : false;
+ }
+
+ static public function superieur_a($value, $test)
+ {
+ return ($value > $test) ? true : false;
+ }
+
+ static public function superieur_ou_egal_a($value, $test)
+ {
+ return ($value >= $test) ? true : false;
+ }
+
+ static public function inferieur_a($value, $test)
+ {
+ return ($value < $test) ? true : false;
+ }
+
+ static public function inferieur_ou_egal_a($value, $test)
+ {
+ return ($value <= $test) ? true : false;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/lib.static_cache.php b/include/lib.static_cache.php
new file mode 100644
index 0000000..d307bd6
--- /dev/null
+++ b/include/lib.static_cache.php
@@ -0,0 +1,87 @@
+ (time() - (int)$expire)) ? false : true;
+ }
+
+ static public function get($id)
+ {
+ $path = self::_getCachePath($id);
+ return file_get_contents($path);
+ }
+
+ static public function display($id)
+ {
+ $path = self::_getCachePath($id);
+ return readfile($path);
+ }
+
+ static public function getPath($id)
+ {
+ return self::_getCachePath($id);
+ }
+
+ static public function remove($id)
+ {
+ $path = self::_getCachePath($id);
+ return unlink($path);
+ }
+
+ static public function clean($expire = self::CLEAN_EXPIRE)
+ {
+ $dir = self::_getCacheDir();
+ $d = dir($dir);
+
+ $expire = time() - $expire;
+
+ while ($file = $d->read())
+ {
+ if ($file[0] == '.')
+ {
+ continue;
+ }
+
+ if (filemtime($dir . '/' . $file) > $expire)
+ {
+ unlink($dir . '/' . $file);
+ }
+ }
+
+ $d->close();
+
+ return true;
+ }
+}
diff --git a/include/lib.template.php b/include/lib.template.php
new file mode 100644
index 0000000..6f9f8e7
--- /dev/null
+++ b/include/lib.template.php
@@ -0,0 +1,604 @@
+cache = false;
+
+ $this->compile_dir = DATA_ROOT . '/cache/compiled';
+ $this->template_dir = ROOT . '/templates';
+
+ $this->compile_check = true;
+
+ $this->reserved_template_varname = 'tpl';
+
+ $this->assign('www_url', WWW_URL);
+ $this->assign('self_url', utils::getSelfUrl());
+
+ $this->assign('is_logged', false);
+ }
+}
+
+$tpl = Template::getInstance();
+
+function tpl_csrf_field($params)
+{
+ $name = utils::CSRF_field_name($params['key']);
+ $value = utils::CSRF_create($params['key']);
+
+ return ' ';
+}
+
+function tpl_form_field($params)
+{
+ if (!isset($params['name']))
+ throw new \BadFunctionCallException('name argument is mandatory');
+
+ $name = $params['name'];
+
+ if (isset($_POST[$name]))
+ $value = $_POST[$name];
+ elseif (isset($params['data']) && isset($params['data'][$name]))
+ $value = $params['data'][$name];
+ elseif (isset($params['default']))
+ $value = $params['default'];
+ else
+ $value = '';
+
+ if (is_array($value))
+ {
+ return $value;
+ }
+
+ if (isset($params['checked']))
+ {
+ if ($value == $params['checked'])
+ return ' checked="checked" ';
+
+ return '';
+ }
+ elseif (isset($params['selected']))
+ {
+ if ($value == $params['selected'])
+ return ' selected="selected" ';
+
+ return '';
+ }
+
+ return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
+}
+
+function tpl_format_tel($n)
+{
+ $n = preg_replace('![^\d\+]!', '', $n);
+
+ if (substr($n, 0, 1) == '+')
+ {
+ $n = preg_replace('!^\+(?:1|2[07]|2\d{2}|3[0-469]|3\d{2}|4[013-9]|'
+ . '4\d{2}|5[1-8]|5\d{2}|6[0-6]|6\d{2}|7\d|8[1-469]|8\d{2}|'
+ . '9[0-58]|9\d{2})!', '\\0 ', $n);
+ }
+ elseif (preg_match('/^\d{10}$/', $n))
+ {
+ $n = preg_replace('!(\d{2})!', '\\1 ', $n);
+ }
+
+ return $n;
+}
+
+function tpl_strftime_fr($ts, $format)
+{
+ return utils::strftime_fr($format, $ts);
+}
+
+function tpl_date_fr($ts, $format)
+{
+ return utils::date_fr($format, $ts);
+}
+
+function tpl_format_droits($params)
+{
+ $droits = $params['droits'];
+
+ $out = ['connexion' => '', 'inscription' => '', 'membres' => '', 'compta' => '',
+ 'wiki' => '', 'config' => ''];
+ $classes = [
+ Membres::DROIT_AUCUN => 'aucun',
+ Membres::DROIT_ACCES => 'acces',
+ Membres::DROIT_ECRITURE=> 'ecriture',
+ Membres::DROIT_ADMIN => 'admin',
+ ];
+
+ foreach ($droits as $cle=>$droit)
+ {
+ $cle = str_replace('droit_', '', $cle);
+
+ if (array_key_exists($cle, $out))
+ {
+
+ $class = $classes[$droit];
+ $desc = false;
+ $s = false;
+
+ if ($cle == 'connexion')
+ {
+ if ($droit == Membres::DROIT_AUCUN)
+ $desc = 'N\'a pas le droit de se connecter';
+ else
+ $desc = 'A le droit de se connecter';
+ }
+ elseif ($cle == 'inscription')
+ {
+ if ($droit == Membres::DROIT_AUCUN)
+ $desc = 'N\'a pas le droit de s\'inscrire seul';
+ else
+ $desc = 'A le droit de s\'inscrire seul';
+ }
+ elseif ($cle == 'config')
+ {
+ $s = '☑';
+
+ if ($droit == Membres::DROIT_AUCUN)
+ $desc = 'Ne peut modifier la configuration';
+ else
+ $desc = 'Peut modifier la configuration';
+ }
+ elseif ($cle == 'compta')
+ {
+ $s = '€';
+ }
+
+ if (!$s)
+ $s = strtoupper($cle[0]);
+
+ if (!$desc)
+ {
+ $desc = ucfirst($cle). ' : ';
+
+ if ($droit == Membres::DROIT_AUCUN)
+ $desc .= 'Pas accès';
+ elseif ($droit == Membres::DROIT_ACCES)
+ $desc .= 'Lecture uniquement';
+ elseif ($droit == Membres::DROIT_ECRITURE)
+ $desc .= 'Lecture & écriture';
+ else
+ $desc .= 'Administration';
+ }
+
+ $out[$cle] = ''.$s.' ';
+ }
+ }
+
+ return implode(' ', $out);
+}
+
+function tpl_format_wiki($str)
+{
+ $str = utils::htmlLinksOnUrls($str);
+ $str = utils::htmlSpip($str);
+ $str = utils::htmlGarbage2xhtml($str);
+ return $str;
+}
+
+function tpl_liens_wiki($str, $prefix)
+{
+ return preg_replace_callback('!!i', function ($matches) use ($prefix) {
+ return ' ';
+ }, $str);
+}
+
+function tpl_pagination($params)
+{
+ if (!isset($params['url']) || !isset($params['page']) || !isset($params['bypage']) || !isset($params['total']))
+ throw new \BadFunctionCallException("Paramètre manquant pour pagination");
+
+ if ($params['total'] == -1)
+ return '';
+
+ $pagination = utils::getGenericPagination($params['page'], $params['total'], $params['bypage']);
+
+ if (empty($pagination))
+ return '';
+
+ $out = '';
+
+ return $out;
+}
+
+function tpl_diff($params)
+{
+ if (!isset($params['old']) || !isset($params['new']))
+ {
+ throw new Template_Exception('Paramètres old et new requis.');
+ }
+
+ $old = $params['old'];
+ $new = $params['new'];
+
+ require_once ROOT . '/include/libs/diff/class.simplediff.php';
+ $diff = \simpleDiff::diff_to_array(false, $old, $new, 3);
+
+ $out = ' ';
+ $prev = key($diff);
+
+ foreach ($diff as $i=>$line)
+ {
+ if ($i > $prev + 1)
+ {
+ $out .= ' ';
+ }
+
+ list($type, $old, $new) = $line;
+
+ $class1 = $class2 = '';
+ $t1 = $t2 = '';
+
+ if ($type == \simpleDiff::INS)
+ {
+ $class2 = 'ins';
+ $t2 = 'â ';
+ $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8');
+ $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8');
+ }
+ elseif ($type == \simpleDiff::DEL)
+ {
+ $class1 = 'del';
+ $t1 = 'â ';
+ $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8');
+ $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8');
+ }
+ elseif ($type == \simpleDiff::CHANGED)
+ {
+ $class1 = 'del';
+ $class2 = 'ins';
+ $t1 = 'â ';
+ $t2 = 'â ';
+
+ $lineDiff = \simpleDiff::wdiff($old, $new);
+ $lineDiff = htmlspecialchars($lineDiff, ENT_QUOTES, 'UTF-8');
+
+ // Don't show new things in deleted line
+ $old = preg_replace('!\{\+(?:.*)\+\}!U', '', $lineDiff);
+ $old = str_replace(' ', ' ', $old);
+ $old = str_replace('-] [-', ' ', $old);
+ $old = preg_replace('!\[-(.*)-\]!U', '\\1', $old);
+
+ // Don't show old things in added line
+ $new = preg_replace('!\[-(?:.*)-\]!U', '', $lineDiff);
+ $new = str_replace(' ', ' ', $new);
+ $new = str_replace('+} {+', ' ', $new);
+ $new = preg_replace('!\{\+(.*)\+\}!U', '\\1 ', $new);
+ }
+ else
+ {
+ $old = htmlspecialchars($old, ENT_QUOTES, 'UTF-8');
+ $new = htmlspecialchars($new, ENT_QUOTES, 'UTF-8');
+ }
+
+ $out .= '';
+ $out .= ''.($i+1).' ';
+ $out .= ''.$t1.' ';
+ $out .= ''.$old.' ';
+ $out .= ''.$t2.' ';
+ $out .= ''.$new.' ';
+ $out .= ' ';
+
+ $prev = $i;
+ }
+
+ $out .= '
';
+ return $out;
+}
+
+function tpl_select_compte($params)
+{
+ $name = $params['name'];
+ $comptes = $params['comptes'];
+ $selected = isset($params['data'][$params['name']]) ? $params['data'][$params['name']] : utils::post($name);
+
+ $out = '';
+
+ foreach ($comptes as $compte)
+ {
+ // Ne pas montrer les comptes désactivés
+ if (!empty($compte['desactive']))
+ continue;
+
+ if (!isset($compte['id'][1]))
+ {
+ $out.= ' ';
+ }
+ elseif (!isset($compte['id'][2]) && empty($params['create']))
+ {
+ if ($compte['id'] > 10)
+ $out.= '';
+
+ $out.= '';
+ }
+ else
+ {
+ $out .= '' . escape_money($number) . '';
+}
+
+function tpl_html_champ_membre($params)
+{
+ if (empty($params['config']) || empty($params['name']))
+ throw new \BadFunctionCallException('Paramètres type et name obligatoires.');
+
+ $config = $params['config'];
+ $type = $config['type'];
+
+ if ($params['name'] == 'passe' || (!empty($params['user_mode']) && !empty($config['private'])))
+ {
+ return '';
+ }
+
+ if ($type == 'select')
+ {
+ if (empty($config['options']))
+ throw new \BadFunctionCallException('Paramètre options obligatoire pour champ de type select.');
+ }
+ elseif ($type == 'country')
+ {
+ $type = 'select';
+ $config['options'] = utils::getCountryList();
+ $params['default'] = Config::getInstance()->get('pays');
+ }
+ elseif ($type == 'date')
+ {
+ $params['pattern'] = '\d{4}-\d{2}-\d{2}';
+ }
+ elseif ($type == 'multiple')
+ {
+ if (empty($config['options']))
+ throw new \BadFunctionCallException('Paramètre options obligatoire pour champ de type multiple.');
+ }
+
+ $field = '';
+ $value = tpl_form_field($params);
+ $attributes = 'name="' . htmlspecialchars($params['name'], ENT_QUOTES, 'UTF-8') . '" ';
+ $attributes .= 'id="f_' . htmlspecialchars($params['name'], ENT_QUOTES, 'UTF-8') . '" ';
+
+ if (!empty($params['disabled']))
+ {
+ $attributes .= 'disabled="disabled" ';
+ }
+
+ if (!empty($config['mandatory']))
+ {
+ $attributes .= 'required="required" ';
+ }
+
+ if (!empty($params['user_mode']) && empty($config['editable']))
+ {
+ $out = '' . htmlspecialchars($config['title'], ENT_QUOTES, 'UTF-8') . ' ';
+ $out .= '' . htmlspecialchars((trim($value) === '' ? 'Non renseigné' : $value), ENT_QUOTES, 'UTF-8') . ' ';
+ return $out;
+ }
+
+ if ($type == 'select')
+ {
+ $field .= '';
+ foreach ($config['options'] as $k=>$v)
+ {
+ if (is_int($k))
+ $k = $v;
+
+ $field .= '';
+ }
+ $field .= ' ';
+ }
+ elseif ($type == 'multiple')
+ {
+ if (is_array($value))
+ {
+ $binary = 0;
+
+ foreach ($value as $k => $v)
+ {
+ if (array_key_exists($k, $config['options']) && !empty($v))
+ {
+ $binary |= 0x01 << $k;
+ }
+ }
+
+ $value = $binary;
+ }
+
+ foreach ($config['options'] as $k=>$v)
+ {
+ $b = 0x01 << (int)$k;
+ $field .= ' '
+ . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . ' ';
+ }
+ }
+ elseif ($type == 'textarea')
+ {
+ $field .= '';
+ }
+ else
+ {
+ if ($type == 'checkbox')
+ {
+ if (!empty($value))
+ {
+ $attributes .= 'checked="checked" ';
+ }
+
+ $value = '1';
+ }
+
+ $field .= ' ';
+ }
+
+ $out = '
+ ';
+
+ if ($type == 'checkbox')
+ {
+ $out .= $field . ' ';
+ }
+
+ $out .= ''
+ . htmlspecialchars($config['title'], ENT_QUOTES, 'UTF-8') . ' ';
+
+ if (!empty($config['mandatory']))
+ {
+ $out .= ' obligatoire ';
+ }
+
+ $out .= ' ';
+
+ if (!empty($config['help']))
+ {
+ $out .= '
+ ' . htmlspecialchars($config['help'], ENT_QUOTES, 'UTF-8') . ' ';
+ }
+
+ if ($type != 'checkbox')
+ {
+ $out .= '
+ ' . $field . ' ';
+ }
+
+ return $out;
+}
+
+$tpl->register_compiler('continue', function() { return 'continue;'; });
+
+$tpl->register_function('csrf_field', 'Garradin\tpl_csrf_field');
+$tpl->register_function('form_field', 'Garradin\tpl_form_field');
+$tpl->register_function('select_compte', 'Garradin\tpl_select_compte');
+
+$tpl->register_function('format_droits', 'Garradin\tpl_format_droits');
+
+$tpl->register_function('pagination', 'Garradin\tpl_pagination');
+
+$tpl->register_function('diff', 'Garradin\tpl_diff');
+$tpl->register_function('html_champ_membre', 'Garradin\tpl_html_champ_membre');
+
+$tpl->register_function('plugin_url', ['Garradin\utils', 'plugin_url']);
+
+$tpl->register_modifier('get_country_name', ['Garradin\utils', 'getCountryName']);
+$tpl->register_modifier('format_tel', 'Garradin\tpl_format_tel');
+$tpl->register_modifier('format_wiki', 'Garradin\tpl_format_wiki');
+$tpl->register_modifier('liens_wiki', 'Garradin\tpl_liens_wiki');
+$tpl->register_modifier('escape_money', 'Garradin\escape_money');
+$tpl->register_modifier('html_money', 'Garradin\tpl_html_money');
+$tpl->register_modifier('abs', 'abs');
+
+$tpl->register_modifier('display_champ_membre', function ($v, $config) {
+ if ($config['type'] == 'checkbox') {
+ return $v ? 'Oui' : 'Non';
+ } elseif ($config['type'] == 'email') {
+ return '' . $v . ' ';
+ } elseif ($config['type'] == 'tel') {
+ return '' . $v . ' ';
+ } elseif ($config['type'] == 'url') {
+ return '' . $v . ' ';
+ } elseif ($config['type'] == 'country') {
+ return utils::getCountryName($v);
+ } elseif ($config['type'] == 'multiple') {
+ $out = [];
+
+ foreach ($config['options'] as $b => $name)
+ {
+ if ($v & (0x01 << $b))
+ $out[] = $name;
+ }
+
+ return implode(', ', $out);
+ } else {
+ return $v;
+ }
+
+});
+
+$tpl->register_modifier('format_sqlite_date_to_french', ['Garradin\utils', 'sqliteDateToFrench']);
+
+$tpl->register_modifier('format_bytes', function ($size) {
+ if ($size > (1024 * 1024))
+ return round($size / 1024 / 1024, 2) . ' Mo';
+ elseif ($size > 1024)
+ return round($size / 1024, 2) . ' Ko';
+ else
+ return $size . ' ob_get_contents(oid)';
+});
+
+$tpl->register_modifier('strftime_fr', 'Garradin\tpl_strftime_fr');
+$tpl->register_modifier('date_fr', 'Garradin\tpl_date_fr');
+
+?>
\ No newline at end of file
diff --git a/include/lib.utils.php b/include/lib.utils.php
new file mode 100644
index 0000000..f134fed
--- /dev/null
+++ b/include/lib.utils.php
@@ -0,0 +1,677 @@
+'Janvier', 'February'=>'Février', 'March'=>'Mars', 'April'=>'Avril', 'May'=>'Mai',
+ 'June'=>'Juin', 'July'=>'Juillet', 'August'=>'Août', 'September'=>'Septembre', 'October'=>'Octobre',
+ 'November'=>'Novembre', 'December'=>'Décembre', 'Monday'=>'Lundi', 'Tuesday'=>'Mardi', 'Wednesday'=>'Mercredi',
+ 'Thursday'=>'Jeudi','Friday'=>'Vendredi','Saturday'=>'Samedi','Sunday'=>'Dimanche',
+ 'Feb'=>'Fév','Apr'=>'Avr','May'=>'Mai','Jun'=>'Juin', 'Jul'=>'Juil','Aug'=>'Aout','Dec'=>'Déc',
+ 'Mon'=>'Lun','Tue'=>'Mar','Wed'=>'Mer','Thu'=>'Jeu','Fri'=>'Ven','Sat'=>'Sam','Sun'=>'Dim'];
+
+ static public function strftime_fr($format=null, $ts=null)
+ {
+ if (is_null($format))
+ {
+ $format = '%d/%m/%Y Ã %H:%M';
+ }
+
+ $date = strftime($format, $ts);
+ $date = strtr($date, self::$french_date_names);
+ $date = strtolower($date);
+ return $date;
+ }
+
+ static public function date_fr($format=null, $ts=null)
+ {
+ if (is_null($format))
+ {
+ $format = 'd/m/Y Ã H:i';
+ }
+
+ $date = date($format, $ts);
+ $date = strtr($date, self::$french_date_names);
+ $date = strtolower($date);
+ return $date;
+ }
+
+ static public function sqliteDateToFrench($d, $short = false)
+ {
+ if (strlen($d) == 10 || $short)
+ {
+ $d = substr($d, 0, 10);
+ $f = 'Y-m-d';
+ $f2 = 'd/m/Y';
+ }
+ elseif (strlen($d) == 16)
+ {
+ $f = 'Y-m-d H:i';
+ $f2 = 'd/m/Y H:i';
+ }
+ else
+ {
+ $f = 'Y-m-d H:i:s';
+ $f2 = 'd/m/Y H:i';
+ }
+
+ if ($dt = \DateTime::createFromFormat($f, $d))
+ return $dt->format($f2);
+ else
+ return $d;
+ }
+
+ static public function makeTimestampFromForm($d)
+ {
+ return mktime($d['h'], $d['min'], 0, $d['m'], $d['d'], $d['y']);
+ }
+
+ static public function modifyDate($str, $change)
+ {
+ $date = \DateTime::createFromFormat('Y-m-d', $str);
+ $date->modify($change);
+ return $date->format('Y-m-d');
+ }
+
+ static public function checkDate($str)
+ {
+ if (!preg_match('!^(\d{4})-(\d{2})-(\d{2})$!', $str, $match))
+ return false;
+
+ if (!checkdate($match[2], $match[3], $match[1]))
+ return false;
+
+ return true;
+ }
+
+ static public function checkDateTime($str)
+ {
+ if (!preg_match('!^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})!', $str, $match))
+ return false;
+
+ if (!self::checkDate($match[1]))
+ return false;
+
+ if ((int) $match[2] < 0 || (int) $match[2] > 23)
+ return false;
+
+ if ((int) $match[3] < 0 || (int) $match[3] > 59)
+ return false;
+
+ if (isset($match[4]) && ((int) $match[4] < 0 || (int) $match[4] > 59))
+ return false;
+
+ return true;
+ }
+
+ static public function getRequestURI()
+ {
+ if (!empty($_SERVER['REQUEST_URI']))
+ return $_SERVER['REQUEST_URI'];
+ else
+ return false;
+ }
+
+ static public function getSelfURL($no_qs = false)
+ {
+ $uri = self::getRequestUri();
+
+ if (strpos($uri, WWW_URI) === 0)
+ {
+ $uri = substr($uri, strlen(WWW_URI));
+ }
+
+ if ($no_qs && (strpos($uri, '?') !== false))
+ {
+ $uri = substr($uri, 0, strpos($uri, '?'));
+ }
+
+ return WWW_URL . $uri;
+ }
+
+ static public function disableHttpCaching()
+ {
+ header("Cache-Control: no-cache, must-revalidate");
+ header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
+ header('Pragma: no-cache');
+ }
+
+
+ public static function redirect($destination=false, $exit=true)
+ {
+ if (empty($destination) || !preg_match('/^https?:\/\//', $destination))
+ {
+ if (empty($destination))
+ $destination = WWW_URL;
+ else
+ $destination = WWW_URL . preg_replace('/^\//', '', $destination);
+ }
+
+ if (headers_sent())
+ {
+ echo
+ ''.
+ ' ' .
+ ' ' .
+ ' '.
+ ' '.
+ ' '.
+ ' '.
+ '';
+
+ if ($exit)
+ exit();
+
+ return true;
+ }
+
+ header("Location: " . $destination);
+
+ if ($exit)
+ exit();
+ }
+
+
+ static protected function _sessionStart($force = false)
+ {
+ if (!isset($_SESSION) && ($force || isset($_COOKIE[session_name()])))
+ {
+ session_start();
+ }
+ return true;
+ }
+
+ static public function CSRF_create($key)
+ {
+ self::_sessionStart(true);
+
+ if (!isset($_SESSION['csrf']))
+ {
+ $_SESSION['csrf'] = [];
+ }
+
+ $_SESSION['csrf'][$key] = sha1($key . uniqid($key, true) . time());
+ return $_SESSION['csrf'][$key];
+ }
+
+ static public function CSRF_check($key, $hash=null)
+ {
+ self::_sessionStart();
+
+ if (is_null($hash))
+ {
+ $name = self::CSRF_field_name($key);
+
+ if (!isset($_POST[$name]))
+ return false;
+
+ $hash = $_POST[$name];
+ }
+
+ if (empty($_SESSION['csrf'][$key]))
+ return false;
+
+ if ($_SESSION['csrf'][$key] != $hash)
+ return false;
+
+ unset($_SESSION['csrf'][$key]);
+
+ return true;
+ }
+
+ static public function CSRF_field_name($key)
+ {
+ return 'gecko/'.base64_encode(sha1($key, true));
+ }
+
+ static public function generatePassword($length, $chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
+ {
+ $string = '';
+ for ($i = 0; $i < $length; $i++)
+ {
+ $pos = rand(0, strlen($chars)-1);
+ $string .= $chars[$pos];
+ }
+ return $string;
+ }
+
+ static public function post($key)
+ {
+ return isset($_POST[$key]) ? $_POST[$key] : '';
+ }
+
+ static public function get($key)
+ {
+ return isset($_GET[$key]) ? $_GET[$key] : '';
+ }
+
+ static public function getIP()
+ {
+ if (!empty($_SERVER['REMOTE_ADDR']))
+ return $_SERVER['REMOTE_ADDR'];
+ return '';
+ }
+
+ static public function &getCountryList()
+ {
+ if (is_null(self::$country_list))
+ {
+ require_once ROOT . '/include/libs/countries/countries_fr.php';
+ self::$country_list = $countries;
+ }
+
+ return self::$country_list;
+ }
+
+ static public function getCountryName($code)
+ {
+ $list = self::getCountryList();
+
+ if (!isset($list[$code]))
+ return false;
+
+ return $list[$code];
+ }
+
+ /**
+ * Génération pagination à partir de la page courante ($current),
+ * du nombre d'items total ($total), et du nombre d'items par page ($bypage).
+ * $listLength représente la longueur d'items de la pagination à génerer
+ *
+ * @param int $current
+ * @param int $total
+ * @param int $bypage
+ * @param int $listLength
+ * @param bool $showLast Toggle l'affichage du dernier élément de la pagination
+ * @return array
+ */
+ public static function getGenericPagination($current, $total, $bypage, $listLength=11, $showLast = true)
+ {
+ if ($total <= $bypage)
+ return false;
+
+ $total = ceil($total / $bypage);
+
+ if ($total < $current)
+ return false;
+
+ $length = ($listLength / 2);
+
+ $begin = $current - ceil($length);
+ if ($begin < 1)
+ {
+ $begin = 1;
+ }
+
+ $end = $begin + $listLength;
+ if($end > $total)
+ {
+ $begin -= ($end - $total);
+ $end = $total;
+ }
+ if ($begin < 1)
+ {
+ $begin = 1;
+ }
+ if($end==($total-1)) {
+ $end = $total;
+ }
+ if($begin == 2) {
+ $begin = 1;
+ }
+ $out = [];
+
+ if ($current > 1) {
+ $out[] = ['id' => $current - 1, 'label' => '« ' . 'Page précédente', 'class' => 'prev', 'accesskey' => 'a'];
+ }
+
+ if ($begin > 1) {
+ $out[] = ['id' => 1, 'label' => '1 ...', 'class' => 'first'];
+ }
+
+ for ($i = $begin; $i <= $end; $i++)
+ {
+ $out[] = ['id' => $i, 'label' => $i, 'class' => ($i == $current) ? 'current' : ''];
+ }
+
+ if ($showLast && $end < $total) {
+ $out[] = ['id' => $total, 'label' => '... ' . $total, 'class' => 'last'];
+ }
+
+ if ($current < $total) {
+ $out[] = ['id' => $current + 1, 'label' => 'Page suivante' . ' »', 'class' => 'next', 'accesskey' => 'z'];
+ }
+
+ return $out;
+ }
+
+ static public function transliterateToAscii($str, $charset='UTF-8')
+ {
+ // Don't process empty strings
+ if (!trim($str))
+ return $str;
+
+ // We only process non-ascii strings
+ if (preg_match('!^[[:ascii:]]+$!', $str))
+ return $str;
+
+ $str = htmlentities($str, ENT_NOQUOTES, $charset);
+
+ $str = preg_replace('#&([A-za-z])(?:acute|cedil|circ|grave|orn|ring|slash|th|tilde|uml);#', '\1', $str);
+ $str = preg_replace('#&([A-za-z]{2})(?:lig);#', '\1', $str); // pour les ligatures e.g. 'œ'
+
+ $str = preg_replace('#&[^;]+;#', '', $str); // supprime les autres caractères
+ $str = preg_replace('![^[:ascii:]]+!', '', $str);
+
+ return $str;
+ }
+
+ static public function htmlLinksOnUrls($str)
+ {
+ return preg_replace_callback('!(?<=\s|^)((?:(ftp|https?|file|ed2k|ircs?)://|(magnet|mailto|data|tel|fax|geo|sips?|xmpp):)([^\s<]+))!',
+ function ($match) {
+ $proto = $match[2] ?: $match[3];
+ $text = ($proto == 'http' || $proto == 'mailto') ? $match[4] : $match[1];
+ return ''.htmlspecialchars($text, ENT_QUOTES, 'UTF-8').' ';
+ }, $str);
+ }
+
+ static public function htmlGarbage2xhtml($str)
+ {
+ if (!self::$g2x)
+ {
+ require_once ROOT . '/include/libs/garbage2xhtml/lib.garbage2xhtml.php';
+ self::$g2x = new \garbage2xhtml;
+ self::$g2x->core_attributes = ['class', 'id', 'title'];
+ }
+
+ return self::$g2x->process($str);
+ }
+
+ static public function htmlSpip($str, $prefix = '')
+ {
+ // Intertitres
+ $str = preg_replace('/(?$1', $str);
+
+ // Gras
+ $str = preg_replace('/(?$1', $str);
+
+ // Italique
+ $str = preg_replace('/(?$1', $str);
+
+ // Espaces typograhiques
+ $str = preg_replace('/\h*([?!;:»])(\s+|$)/u', ' $1$2', $str);
+ $str = preg_replace('/(^|\s+)([«])\h*/u', '$1$2 ', $str);
+
+ // Liens
+ $str = preg_replace('/(?(.+?)\]/', '$1 ', $str);
+ $str = preg_replace('/(?$1', $str);
+
+ // Adresses email
+ $str = preg_replace('//iU', ' ', $str);
+
+ return $str;
+ }
+
+ static public function mail($to, $subject, $content, $additional_headers = [])
+ {
+ // Création du contenu du message
+ $content = wordwrap($content);
+ $content = trim($content);
+
+ $content = preg_replace("#(?get('email_envoi_automatique').'>';
+ }
+
+ $additional_headers['MIME-Version'] = '1.0';
+ $additional_headers['Content-type'] = 'text/plain; charset=UTF-8';
+ $additional_headers['Return-Path'] = $config->get('email_envoi_automatique');
+
+ foreach ($additional_headers as $name=>$value)
+ {
+ $headers .= $name . ': '.$value."\r\n";
+ }
+
+ $headers = preg_replace("#(?read())
+ {
+ if ($file[0] != '.')
+ {
+ unlink($path . '/' . $file);
+ }
+ }
+
+ $dir->close();
+ return true;
+ }
+
+ static public function suggestPassword()
+ {
+ require_once ROOT . '/include/libs/passphrase/lib.passphrase.french.php';
+ return \Passphrase::generate();
+ }
+
+ static public function checkIBAN($iban)
+ {
+ $iban = substr($iban, 4) . substr($iban, 0, 4);
+ $iban = str_replace(range('A', 'Z'), range(10, 35), $iban);
+ return (bcmod($iban, 97) == 1);
+ }
+
+ static public function IBAN_RIB($iban)
+ {
+ if (substr($iban, 0, 2) != 'FR')
+ {
+ return '';
+ }
+
+ return substr($iban, 4, 5) // Code banque
+ . ' ' . substr($iban, 4+5, 5) // Code guichet
+ . ' ' . substr($iban, 4+5+5, -2) // Numéro de compte
+ . ' ' . substr($iban, -2); // Clé RIB
+ }
+
+ static public function checkBIC($bic)
+ {
+ return preg_match('!^[A-Z]{4}[A-Z]{2}[1-9A-Z]{2}(?:[A-Z\d]{3})?$!', $bic);
+ }
+
+ static public function normalizePhoneNumber($n)
+ {
+ $n = preg_replace('!(\+\d+)\(0\)!', '\\1', $n);
+ $n = preg_replace('![^\d\+]!', '', $n);
+ return $n;
+ }
+
+ static public function write_ini_string($in)
+ {
+ $out = '';
+ $get_ini_line = function ($key, $value) use (&$get_ini_line)
+ {
+ if (is_bool($value))
+ {
+ return $key . ' = ' . ($value ? 'true' : 'false');
+ }
+ elseif (is_numeric($value))
+ {
+ return $key . ' = ' . $value;
+ }
+ elseif (is_array($value))
+ {
+ $out = '';
+ foreach ($value as $row)
+ {
+ $out .= $get_ini_line($key . '[]', $row) . "\n";
+ }
+
+ return substr($out, 0, -1);
+ }
+ else
+ {
+ return $key . ' = "' . str_replace('"', '\\"', $value) . '"';
+ }
+ };
+
+ foreach ($in as $key=>$value)
+ {
+ if (is_array($value) && is_string($key))
+ {
+ $out .= '[' . $key . "]\n";
+
+ foreach ($value as $row_key=>$row_value)
+ {
+ $out .= $get_ini_line($row_key, $row_value) . "\n";
+ }
+
+ $out .= "\n";
+ }
+ else
+ {
+ $out .= $get_ini_line($key, $value) . "\n";
+ }
+ }
+
+ return $out;
+ }
+
+ static public function getMaxUploadSize()
+ {
+ return min([
+ self::return_bytes(ini_get('upload_max_filesize')),
+ self::return_bytes(ini_get('post_max_size')),
+ self::return_bytes(ini_get('memory_limit'))
+ ]);
+ }
+
+
+ static public function return_bytes ($size_str)
+ {
+ switch (substr($size_str, -1))
+ {
+ case 'G': case 'g': return (int)$size_str * pow(1024, 3);
+ case 'M': case 'm': return (int)$size_str * pow(1024, 2);
+ case 'K': case 'k': return (int)$size_str * 1024;
+ default: return $size_str;
+ }
+ }
+
+ static public function deleteRecursive($path, $delete_target = false)
+ {
+ if (!file_exists($path))
+ return false;
+
+ $dir = dir($path);
+ if (!$dir) return false;
+
+ while ($file = $dir->read())
+ {
+ if ($file == '.' || $file == '..')
+ continue;
+
+ if (is_dir($path . '/' . $file))
+ {
+ if (!self::deleteRecursive($path . '/' . $file, true))
+ return false;
+ }
+ else
+ {
+ unlink($path . '/' . $file);
+ }
+ }
+
+ $dir->close();
+ rmdir($path);
+
+ return true;
+ }
+
+ static public function plugin_url($params = [])
+ {
+ if (isset($params['id']))
+ {
+ $url = WWW_URL . 'admin/plugin/' . $params['id'] . '/';
+ }
+ else
+ {
+ $url = PLUGIN_URL;
+ }
+
+ if (!empty($params['file']))
+ $url .= $params['file'];
+
+ if (!empty($params['query']))
+ {
+ $url .= '?';
+
+ if (!(is_numeric($params['query']) && (int)$params['query'] === 1) && $params['query'] !== true)
+ $url .= $params['query'];
+ }
+
+ return $url;
+ }
+
+ static public function find_csv_delim($fp)
+ {
+ $line = '';
+
+ while ($line === '' && !feof($fp))
+ {
+ $line = trim(fgets($fp, 4096));
+ }
+
+ // Delete the columns content
+ $line = preg_replace('/".*?"/', '', $line);
+
+ $delims = [
+ ';' => substr_count($line, ';'),
+ ',' => substr_count($line, ','),
+ "\t"=> substr_count($line, "\t")
+ ];
+
+ arsort($delims);
+ reset($delims);
+
+ rewind($fp);
+ return key($delims);
+ }
+
+}
diff --git a/include/libs/countries/countries_en.php b/include/libs/countries/countries_en.php
new file mode 100644
index 0000000..de27ae3
--- /dev/null
+++ b/include/libs/countries/countries_en.php
@@ -0,0 +1,261 @@
+ 'Afghanistan',
+ 'AX' => 'Ã
land Islands',
+ 'AL' => 'Albania',
+ 'DZ' => 'Algeria',
+ 'AS' => 'American Samoa',
+ 'AD' => 'Andorra',
+ 'AO' => 'Angola',
+ 'AI' => 'Anguilla',
+ 'AQ' => 'Antarctica',
+ 'AG' => 'Antigua And Barbuda',
+ 'AR' => 'Argentina',
+ 'AM' => 'Armenia',
+ 'AW' => 'Aruba',
+ 'AU' => 'Australia',
+ 'AT' => 'Austria',
+ 'AZ' => 'Azerbaijan',
+ 'BS' => 'Bahamas',
+ 'BH' => 'Bahrain',
+ 'BD' => 'Bangladesh',
+ 'BB' => 'Barbados',
+ 'BY' => 'Belarus',
+ 'BE' => 'Belgium',
+ 'BZ' => 'Belize',
+ 'BJ' => 'Benin',
+ 'BM' => 'Bermuda',
+ 'BT' => 'Bhutan',
+ 'BO' => 'Bolivia, Plurinational State Of',
+ 'BQ' => 'Bonaire, Sint Eustatius And Saba',
+ 'BA' => 'Bosnia And Herzegovina',
+ 'BW' => 'Botswana',
+ 'BV' => 'Bouvet Island',
+ 'BR' => 'Brazil',
+ 'IO' => 'British Indian Ocean Territory',
+ 'BN' => 'Brunei Darussalam',
+ 'BG' => 'Bulgaria',
+ 'BF' => 'Burkina Faso',
+ 'BI' => 'Burundi',
+ 'KH' => 'Cambodia',
+ 'CM' => 'Cameroon',
+ 'CA' => 'Canada',
+ 'CV' => 'Cape Verde',
+ 'KY' => 'Cayman Islands',
+ 'CF' => 'Central African Republic',
+ 'TD' => 'Chad',
+ 'CL' => 'Chile',
+ 'CN' => 'China',
+ 'CX' => 'Christmas Island',
+ 'CC' => 'Cocos (keeling) Islands',
+ 'CO' => 'Colombia',
+ 'KM' => 'Comoros',
+ 'CG' => 'Congo',
+ 'CD' => 'Congo, The Democratic Republic Of The',
+ 'CK' => 'Cook Islands',
+ 'CR' => 'Costa Rica',
+ 'CI' => 'CÃte D\'ivoire',
+ 'HR' => 'Croatia',
+ 'CU' => 'Cuba',
+ 'CW' => 'CuraÃao',
+ 'CY' => 'Cyprus',
+ 'CZ' => 'Czech Republic',
+ 'DK' => 'Denmark',
+ 'DJ' => 'Djibouti',
+ 'DM' => 'Dominica',
+ 'DO' => 'Dominican Republic',
+ 'EC' => 'Ecuador',
+ 'EG' => 'Egypt',
+ 'SV' => 'El Salvador',
+ 'GQ' => 'Equatorial Guinea',
+ 'ER' => 'Eritrea',
+ 'EE' => 'Estonia',
+ 'ET' => 'Ethiopia',
+ 'FK' => 'Falkland Islands (malvinas)',
+ 'FO' => 'Faroe Islands',
+ 'FJ' => 'Fiji',
+ 'FI' => 'Finland',
+ 'FR' => 'France',
+ 'GF' => 'French Guiana',
+ 'PF' => 'French Polynesia',
+ 'TF' => 'French Southern Territories',
+ 'GA' => 'Gabon',
+ 'GM' => 'Gambia',
+ 'GE' => 'Georgia',
+ 'DE' => 'Germany',
+ 'GH' => 'Ghana',
+ 'GI' => 'Gibraltar',
+ 'GR' => 'Greece',
+ 'GL' => 'Greenland',
+ 'GD' => 'Grenada',
+ 'GP' => 'Guadeloupe',
+ 'GU' => 'Guam',
+ 'GT' => 'Guatemala',
+ 'GG' => 'Guernsey',
+ 'GN' => 'Guinea',
+ 'GW' => 'Guinea-bissau',
+ 'GY' => 'Guyana',
+ 'HT' => 'Haiti',
+ 'HM' => 'Heard Island And Mcdonald Islands',
+ 'VA' => 'Holy See (vatican City State)',
+ 'HN' => 'Honduras',
+ 'HK' => 'Hong Kong',
+ 'HU' => 'Hungary',
+ 'IS' => 'Iceland',
+ 'IN' => 'India',
+ 'ID' => 'Indonesia',
+ 'IR' => 'Iran, Islamic Republic Of',
+ 'IQ' => 'Iraq',
+ 'IE' => 'Ireland',
+ 'IM' => 'Isle Of Man',
+ 'IL' => 'Israel',
+ 'IT' => 'Italy',
+ 'JM' => 'Jamaica',
+ 'JP' => 'Japan',
+ 'JE' => 'Jersey',
+ 'JO' => 'Jordan',
+ 'KZ' => 'Kazakhstan',
+ 'KE' => 'Kenya',
+ 'KI' => 'Kiribati',
+ 'KP' => 'Korea, Democratic People\'s Republic Of',
+ 'KR' => 'Korea, Republic Of',
+ 'KW' => 'Kuwait',
+ 'KG' => 'Kyrgyzstan',
+ 'LA' => 'Lao People\'s Democratic Republic',
+ 'LV' => 'Latvia',
+ 'LB' => 'Lebanon',
+ 'LS' => 'Lesotho',
+ 'LR' => 'Liberia',
+ 'LY' => 'Libya',
+ 'LI' => 'Liechtenstein',
+ 'LT' => 'Lithuania',
+ 'LU' => 'Luxembourg',
+ 'MO' => 'Macao',
+ 'MK' => 'Macedonia, The Former Yugoslav Republic Of',
+ 'MG' => 'Madagascar',
+ 'MW' => 'Malawi',
+ 'MY' => 'Malaysia',
+ 'MV' => 'Maldives',
+ 'ML' => 'Mali',
+ 'MT' => 'Malta',
+ 'MH' => 'Marshall Islands',
+ 'MQ' => 'Martinique',
+ 'MR' => 'Mauritania',
+ 'MU' => 'Mauritius',
+ 'YT' => 'Mayotte',
+ 'MX' => 'Mexico',
+ 'FM' => 'Micronesia, Federated States Of',
+ 'MD' => 'Moldova, Republic Of',
+ 'MC' => 'Monaco',
+ 'MN' => 'Mongolia',
+ 'ME' => 'Montenegro',
+ 'MS' => 'Montserrat',
+ 'MA' => 'Morocco',
+ 'MZ' => 'Mozambique',
+ 'MM' => 'Myanmar',
+ 'NA' => 'Namibia',
+ 'NR' => 'Nauru',
+ 'NP' => 'Nepal',
+ 'NL' => 'Netherlands',
+ 'NC' => 'New Caledonia',
+ 'NZ' => 'New Zealand',
+ 'NI' => 'Nicaragua',
+ 'NE' => 'Niger',
+ 'NG' => 'Nigeria',
+ 'NU' => 'Niue',
+ 'NF' => 'Norfolk Island',
+ 'MP' => 'Northern Mariana Islands',
+ 'NO' => 'Norway',
+ 'OM' => 'Oman',
+ 'PK' => 'Pakistan',
+ 'PW' => 'Palau',
+ 'PS' => 'Palestinian Territory, Occupied',
+ 'PA' => 'Panama',
+ 'PG' => 'Papua New Guinea',
+ 'PY' => 'Paraguay',
+ 'PE' => 'Peru',
+ 'PH' => 'Philippines',
+ 'PN' => 'Pitcairn',
+ 'PL' => 'Poland',
+ 'PT' => 'Portugal',
+ 'PR' => 'Puerto Rico',
+ 'QA' => 'Qatar',
+ 'RE' => 'RÃunion',
+ 'RO' => 'Romania',
+ 'RU' => 'Russian Federation',
+ 'RW' => 'Rwanda',
+ 'BL' => 'Saint BarthÃlemy',
+ 'SH' => 'Saint Helena, Ascension And Tristan Da Cunha',
+ 'KN' => 'Saint Kitts And Nevis',
+ 'LC' => 'Saint Lucia',
+ 'MF' => 'Saint Martin (french Part)',
+ 'PM' => 'Saint Pierre And Miquelon',
+ 'VC' => 'Saint Vincent And The Grenadines',
+ 'WS' => 'Samoa',
+ 'SM' => 'San Marino',
+ 'ST' => 'Sao Tome And Principe',
+ 'SA' => 'Saudi Arabia',
+ 'SN' => 'Senegal',
+ 'RS' => 'Serbia',
+ 'SC' => 'Seychelles',
+ 'SL' => 'Sierra Leone',
+ 'SG' => 'Singapore',
+ 'SX' => 'Sint Maarten (dutch Part)',
+ 'SK' => 'Slovakia',
+ 'SI' => 'Slovenia',
+ 'SB' => 'Solomon Islands',
+ 'SO' => 'Somalia',
+ 'ZA' => 'South Africa',
+ 'GS' => 'South Georgia And The South Sandwich Islands',
+ 'SS' => 'South Sudan',
+ 'ES' => 'Spain',
+ 'LK' => 'Sri Lanka',
+ 'SD' => 'Sudan',
+ 'SR' => 'Suriname',
+ 'SJ' => 'Svalbard And Jan Mayen',
+ 'SZ' => 'Swaziland',
+ 'SE' => 'Sweden',
+ 'CH' => 'Switzerland',
+ 'SY' => 'Syrian Arab Republic',
+ 'TW' => 'Taiwan, Province Of China',
+ 'TJ' => 'Tajikistan',
+ 'TZ' => 'Tanzania, United Republic Of',
+ 'TH' => 'Thailand',
+ 'TL' => 'Timor-leste',
+ 'TG' => 'Togo',
+ 'TK' => 'Tokelau',
+ 'TO' => 'Tonga',
+ 'TT' => 'Trinidad And Tobago',
+ 'TN' => 'Tunisia',
+ 'TR' => 'Turkey',
+ 'TM' => 'Turkmenistan',
+ 'TC' => 'Turks And Caicos Islands',
+ 'TV' => 'Tuvalu',
+ 'UG' => 'Uganda',
+ 'UA' => 'Ukraine',
+ 'AE' => 'United Arab Emirates',
+ 'GB' => 'United Kingdom',
+ 'US' => 'United States',
+ 'UM' => 'United States Minor Outlying Islands',
+ 'UY' => 'Uruguay',
+ 'UZ' => 'Uzbekistan',
+ 'VU' => 'Vanuatu',
+ 'VE' => 'Venezuela, Bolivarian Republic Of',
+ 'VN' => 'Viet Nam',
+ 'VG' => 'Virgin Islands, British',
+ 'VI' => 'Virgin Islands, U.s.',
+ 'WF' => 'Wallis And Futuna',
+ 'EH' => 'Western Sahara',
+ 'YE' => 'Yemen',
+ 'ZM' => 'Zambia',
+ 'ZW' => 'Zimbabwe',
+);
+
+?>
\ No newline at end of file
diff --git a/include/libs/countries/countries_fr.php b/include/libs/countries/countries_fr.php
new file mode 100644
index 0000000..f5a131f
--- /dev/null
+++ b/include/libs/countries/countries_fr.php
@@ -0,0 +1,260 @@
+ 'Afghanistan',
+ 'ZA' => 'Afrique Du Sud',
+ 'AX' => 'Ã
land, Ãles',
+ 'AL' => 'Albanie',
+ 'DZ' => 'Algérie',
+ 'DE' => 'Allemagne',
+ 'AD' => 'Andorre',
+ 'AO' => 'Angola',
+ 'AI' => 'Anguilla',
+ 'AQ' => 'Antarctique',
+ 'AG' => 'Antigua-et-barbuda',
+ 'SA' => 'Arabie Saoudite',
+ 'AR' => 'Argentine',
+ 'AM' => 'Arménie',
+ 'AW' => 'Aruba',
+ 'AU' => 'Australie',
+ 'AT' => 'Autriche',
+ 'AZ' => 'Azerbaïdjan',
+ 'BS' => 'Bahamas',
+ 'BH' => 'Bahreïn',
+ 'BD' => 'Bangladesh',
+ 'BB' => 'Barbade',
+ 'BY' => 'Bélarus',
+ 'BE' => 'Belgique',
+ 'BZ' => 'Belize',
+ 'BJ' => 'Bénin',
+ 'BM' => 'Bermudes',
+ 'BT' => 'Bhoutan',
+ 'BO' => 'Bolivie, L\'état Plurinational De',
+ 'BQ' => 'Bonaire, Saint-eustache Et Saba',
+ 'BA' => 'Bosnie-herzégovine',
+ 'BW' => 'Botswana',
+ 'BV' => 'Bouvet, Ãle',
+ 'BR' => 'Brésil',
+ 'BN' => 'Brunei Darussalam',
+ 'BG' => 'Bulgarie',
+ 'BF' => 'Burkina Faso',
+ 'BI' => 'Burundi',
+ 'KY' => 'Caïmans, Ãles',
+ 'KH' => 'Cambodge',
+ 'CM' => 'Cameroun',
+ 'CA' => 'Canada',
+ 'CV' => 'Cap-vert',
+ 'CF' => 'Centrafricaine, République',
+ 'CL' => 'Chili',
+ 'CN' => 'Chine',
+ 'CX' => 'Christmas, Ãle',
+ 'CY' => 'Chypre',
+ 'CC' => 'Cocos (keeling), Ãles',
+ 'CO' => 'Colombie',
+ 'KM' => 'Comores',
+ 'CG' => 'Congo',
+ 'CD' => 'Congo, La République Démocratique Du',
+ 'CK' => 'Cook, Ãles',
+ 'KR' => 'Corée, République De',
+ 'KP' => 'Corée, République Populaire Démocratique De',
+ 'CR' => 'Costa Rica',
+ 'CI' => 'Côte D\'ivoire',
+ 'HR' => 'Croatie',
+ 'CU' => 'Cuba',
+ 'CW' => 'Curaçao',
+ 'DK' => 'Danemark',
+ 'DJ' => 'Djibouti',
+ 'DO' => 'Dominicaine, République',
+ 'DM' => 'Dominique',
+ 'EG' => 'Ãgypte',
+ 'SV' => 'El Salvador',
+ 'AE' => 'Ãmirats Arabes Unis',
+ 'EC' => 'Ãquateur',
+ 'ER' => 'Ãrythrée',
+ 'ES' => 'Espagne',
+ 'EE' => 'Estonie',
+ 'US' => 'Ãtats-unis',
+ 'ET' => 'Ãthiopie',
+ 'FK' => 'Falkland, Ãles (malvinas)',
+ 'FO' => 'Féroé, Ãles',
+ 'FJ' => 'Fidji',
+ 'FI' => 'Finlande',
+ 'FR' => 'France',
+ 'GA' => 'Gabon',
+ 'GM' => 'Gambie',
+ 'GE' => 'Géorgie',
+ 'GS' => 'Géorgie Du Sud-et-les Ãles Sandwich Du Sud',
+ 'GH' => 'Ghana',
+ 'GI' => 'Gibraltar',
+ 'GR' => 'Grèce',
+ 'GD' => 'Grenade',
+ 'GL' => 'Groenland',
+ 'GP' => 'Guadeloupe',
+ 'GU' => 'Guam',
+ 'GT' => 'Guatemala',
+ 'GG' => 'Guernesey',
+ 'GN' => 'Guinée',
+ 'GW' => 'Guinée-bissau',
+ 'GQ' => 'Guinée Ãquatoriale',
+ 'GY' => 'Guyana',
+ 'GF' => 'Guyane Française',
+ 'HT' => 'Haïti',
+ 'HM' => 'Heard-et-îles Macdonald, Ãle',
+ 'HN' => 'Honduras',
+ 'HK' => 'Hong Kong',
+ 'HU' => 'Hongrie',
+ 'IM' => 'Ãle De Man',
+ 'UM' => 'Ãles Mineures Ãloignées Des Ãtats-unis',
+ 'VG' => 'Ãles Vierges Britanniques',
+ 'VI' => 'Ãles Vierges Des Ãtats-unis',
+ 'IN' => 'Inde',
+ 'ID' => 'Indonésie',
+ 'IR' => 'Iran, République Islamique D\'',
+ 'IQ' => 'Iraq',
+ 'IE' => 'Irlande',
+ 'IS' => 'Islande',
+ 'IL' => 'Israël',
+ 'IT' => 'Italie',
+ 'JM' => 'Jamaïque',
+ 'JP' => 'Japon',
+ 'JE' => 'Jersey',
+ 'JO' => 'Jordanie',
+ 'KZ' => 'Kazakhstan',
+ 'KE' => 'Kenya',
+ 'KG' => 'Kirghizistan',
+ 'KI' => 'Kiribati',
+ 'KW' => 'Koweït',
+ 'LA' => 'Lao, République Démocratique Populaire',
+ 'LS' => 'Lesotho',
+ 'LV' => 'Lettonie',
+ 'LB' => 'Liban',
+ 'LR' => 'Libéria',
+ 'LY' => 'Libye',
+ 'LI' => 'Liechtenstein',
+ 'LT' => 'Lituanie',
+ 'LU' => 'Luxembourg',
+ 'MO' => 'Macao',
+ 'MK' => 'Macédoine, L\'ex-république Yougoslave De',
+ 'MG' => 'Madagascar',
+ 'MY' => 'Malaisie',
+ 'MW' => 'Malawi',
+ 'MV' => 'Maldives',
+ 'ML' => 'Mali',
+ 'MT' => 'Malte',
+ 'MP' => 'Mariannes Du Nord, Ãles',
+ 'MA' => 'Maroc',
+ 'MH' => 'Marshall, Ãles',
+ 'MQ' => 'Martinique',
+ 'MU' => 'Maurice',
+ 'MR' => 'Mauritanie',
+ 'YT' => 'Mayotte',
+ 'MX' => 'Mexique',
+ 'FM' => 'Micronésie, Ãtats Fédérés De',
+ 'MD' => 'Moldova, République De',
+ 'MC' => 'Monaco',
+ 'MN' => 'Mongolie',
+ 'ME' => 'Monténégro',
+ 'MS' => 'Montserrat',
+ 'MZ' => 'Mozambique',
+ 'MM' => 'Myanmar',
+ 'NA' => 'Namibie',
+ 'NR' => 'Nauru',
+ 'NP' => 'Népal',
+ 'NI' => 'Nicaragua',
+ 'NE' => 'Niger',
+ 'NG' => 'Nigéria',
+ 'NU' => 'Niué',
+ 'NF' => 'Norfolk, Ãle',
+ 'NO' => 'Norvège',
+ 'NC' => 'Nouvelle-calédonie',
+ 'NZ' => 'Nouvelle-zélande',
+ 'IO' => 'Océan Indien, Territoire Britannique De L\'',
+ 'OM' => 'Oman',
+ 'UG' => 'Ouganda',
+ 'UZ' => 'Ouzbékistan',
+ 'PK' => 'Pakistan',
+ 'PW' => 'Palaos',
+ 'PS' => 'Palestinien Occupé, Territoire',
+ 'PA' => 'Panama',
+ 'PG' => 'Papouasie-nouvelle-guinée',
+ 'PY' => 'Paraguay',
+ 'NL' => 'Pays-bas',
+ 'PE' => 'Pérou',
+ 'PH' => 'Philippines',
+ 'PN' => 'Pitcairn',
+ 'PL' => 'Pologne',
+ 'PF' => 'Polynésie Française',
+ 'PR' => 'Porto Rico',
+ 'PT' => 'Portugal',
+ 'QA' => 'Qatar',
+ 'RE' => 'Réunion',
+ 'RO' => 'Roumanie',
+ 'GB' => 'Royaume-uni',
+ 'RU' => 'Russie, Fédération De',
+ 'RW' => 'Rwanda',
+ 'EH' => 'Sahara Occidental',
+ 'BL' => 'Saint-barthélemy',
+ 'SH' => 'Sainte-hélène, Ascension Et Tristan Da Cunha',
+ 'LC' => 'Sainte-lucie',
+ 'KN' => 'Saint-kitts-et-nevis',
+ 'SM' => 'Saint-marin',
+ 'MF' => 'Saint-martin(partie Française)',
+ 'SX' => 'Saint-martin (partie Néerlandaise)',
+ 'PM' => 'Saint-pierre-et-miquelon',
+ 'VA' => 'Saint-siège (état De La Cité Du Vatican)',
+ 'VC' => 'Saint-vincent-et-les Grenadines',
+ 'SB' => 'Salomon, Ãles',
+ 'WS' => 'Samoa',
+ 'AS' => 'Samoa Américaines',
+ 'ST' => 'Sao Tomé-et-principe',
+ 'SN' => 'Sénégal',
+ 'RS' => 'Serbie',
+ 'SC' => 'Seychelles',
+ 'SL' => 'Sierra Leone',
+ 'SG' => 'Singapour',
+ 'SK' => 'Slovaquie',
+ 'SI' => 'Slovénie',
+ 'SO' => 'Somalie',
+ 'SD' => 'Soudan',
+ 'SS' => 'Soudan Du Sud',
+ 'LK' => 'Sri Lanka',
+ 'SE' => 'Suède',
+ 'CH' => 'Suisse',
+ 'SR' => 'Suriname',
+ 'SJ' => 'Svalbard Et Ãle Jan Mayen',
+ 'SZ' => 'Swaziland',
+ 'SY' => 'Syrienne, République Arabe',
+ 'TJ' => 'Tadjikistan',
+ 'TW' => 'Taïwan, Province De Chine',
+ 'TZ' => 'Tanzanie, République-unie De',
+ 'TD' => 'Tchad',
+ 'CZ' => 'Tchèque, République',
+ 'TF' => 'Terres Australes Françaises',
+ 'TH' => 'Thaïlande',
+ 'TL' => 'Timor-leste',
+ 'TG' => 'Togo',
+ 'TK' => 'Tokelau',
+ 'TO' => 'Tonga',
+ 'TT' => 'Trinité-et-tobago',
+ 'TN' => 'Tunisie',
+ 'TM' => 'Turkménistan',
+ 'TC' => 'Turks-et-caïcos, Ãles',
+ 'TR' => 'Turquie',
+ 'TV' => 'Tuvalu',
+ 'UA' => 'Ukraine',
+ 'UY' => 'Uruguay',
+ 'VU' => 'Vanuatu',
+ 'VE' => 'Venezuela, République Bolivarienne Du',
+ 'VN' => 'Viet Nam',
+ 'WF' => 'Wallis Et Futuna',
+ 'YE' => 'Yémen',
+ 'ZM' => 'Zambie',
+ 'ZW' => 'Zimbabwe',
+);
+
+?>
\ No newline at end of file
diff --git a/include/libs/diff/class.simplediff.php b/include/libs/diff/class.simplediff.php
new file mode 100644
index 0000000..d59fe51
--- /dev/null
+++ b/include/libs/diff/class.simplediff.php
@@ -0,0 +1,414 @@
+
+ Copyright (C) 2005 Nils Knappmeier next version
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+ http://www.gnu.org/licenses/gpl.html
+
+ About:
+ I searched a function to compare arrays and the array_diff()
+ was not specific enough. It ignores the order of the array-values.
+ So I reimplemented the diff-function which is found on unix-systems
+ but this you can use directly in your code and adopt for your needs.
+ Simply adopt the formatline-function. with the third-parameter of arr_diff()
+ you can hide matching lines. Hope someone has use for this.
+
+ Contact: d.u.diff@holomind.de
+ **/
+
+ # split the source text into arrays of lines
+ if (is_array($old))
+ $t1 = $old;
+ else
+ $t1 = explode("\n",$old);
+
+ $x = array_pop($t1);
+ if ($x>'') $t1[]="$x\n\\ No newline at end of file";
+
+ if (is_array($new))
+ $t2 = $new;
+ else
+ $t2 = explode("\n",$new);
+
+ $x=array_pop($t2);
+ if ($x>'') $t2[]="$x\n\\ No newline at end of file";
+
+ # build a reverse-index array using the line as key and line number as value
+ # don't store blank lines, so they won't be targets of the shortest distance
+ # search
+ foreach($t1 as $i=>$x)
+ {
+ if ($x>'') $r1[$x][]=$i;
+ }
+ foreach($t2 as $i=>$x) if ($x>'') $r2[$x][]=$i;
+
+ $a1=0; $a2=0; # start at beginning of each list
+ $actions=array();
+
+ # walk this loop until we reach the end of one of the lists
+ while ($a1=$s1) { $d=$n; break; }
+ }
+ }
+ if ($d>=$s1 && ($d+$s2-$a1-$a2)<($best1+$best2-$a1-$a2))
+ { $best1=$d; $best2=$s2; }
+ $d=-1;
+ if (isset($t1[$s1]) && isset($r2[$t1[$s1]]))
+ {
+ foreach((array)@$r2[$t1[$s1]] as $n)
+ {
+ if ($n>=$s2) { $d=$n; break; }
+ }
+ }
+ if ($d>=$s2 && ($s1+$d-$a1-$a2)<($best1+$best2-$a1-$a2))
+ { $best1=$s1; $best2=$d; }
+ $s1++; $s2++;
+ }
+ while ($a1<$best1) { $actions[]=1; $a1++; } # deleted elements
+ while ($a2<$best2) { $actions[]=2; $a2++; } # added elements
+ }
+
+ # we've reached the end of one list, now walk to the end of the other
+ while($a10) {
+ $xstr = ($x1==($x0+1)) ? $x1 : ($x0+1).",$x1";
+ $ystr = ($y1==($y0+1)) ? $y1 : ($y0+1).",$y1";
+ if ($op==1) $out[] = "{$xstr}d{$y1}";
+ elseif ($op==3) $out[] = "{$xstr}c{$ystr}";
+ while ($x0<$x1) { $out[] = '< '.$t1[$x0]; $x0++; } # deleted elems
+ if ($op==2) $out[] = "{$x1}a{$ystr}";
+ elseif ($op==3) $out[] = '---';
+ while ($y0<$y1) { $out[] = '> '.$t2[$y0]; $y0++; } # added elems
+ }
+ $x1++; $x0=$x1;
+ $y1++; $y0=$y1;
+ $op=0;
+ }
+ $out[] = '';
+
+ if ($return_as_array)
+ return $out;
+ else
+ return implode("\n",$out);
+ }
+
+ /**
+ * Applies a diff to a text
+ *
+ * @param string $original Original text to patch
+ * @param string $patch Diff text
+ * @param bool $return_as_array Returns the patched text as an array
+ */
+ static public function patch($original, $patch, $return_as_array = false)
+ {
+ $new = array();
+
+ if (!is_array($patch))
+ $patch = explode("\n", $patch);
+
+ if (!is_array($original))
+ $original = explode("\n", str_replace("\r", "", $original));
+
+ $i = 0;
+ foreach ($patch as $line)
+ {
+ if (empty($line))
+ continue;
+
+ $line = str_replace("\n\\ No newline at end of file", "", $line);
+
+ if ($line[0] == '>')
+ {
+ $new[] = substr($line, 2);
+ }
+ elseif (preg_match('!^(?P[0-9]+)(?:,(?P[0-9]+))?(?P[acd])(?P[0-9]+)(?:,(?[0-9]+))?$!', trim($line), $match))
+ {
+ $sub = ($match['mode'] == 'a') ? 0 : 1;
+ for ($a = $i; $a < ($match['ob'] - $sub); $a++)
+ {
+ $new[] = $original[$a];
+ }
+ $i = $match['oe'] ? (int) $match['oe'] : (int) $match['ob'];
+ }
+ }
+ for ($a = $i; $a < count($original); $a++)
+ {
+ $new[] = $original[$a];
+ }
+
+ return $return_as_array ? $new : implode("\n", $new);
+ }
+
+ /**
+ * Returns an array showing differences between two arrays
+ *
+ * @param string $diff Diff text, set to false and the diff will be made from $old and $new
+ * @param string $old Old text
+ * @param string $new New text, could be set to false if the diff is supplied
+ * @param bool $show_context Include context in the array? Set to false to avoid context,
+ set to true to have all the context and set to an (int) to have this number of lines of
+ context before and after each modified line
+ */
+ static public function diff_to_array($diff = false, $old, $new = false, $show_context = true)
+ {
+ if ($diff === false && $new === false)
+ {
+ throw new Exception("diff_to_array needs either the diff text or the new text file");
+ }
+
+ if ($diff === false)
+ {
+ $diff = self::diff($old, $new, true);
+ }
+
+ if (!is_array($diff))
+ $old = explode("\n", $diff);
+
+ if (!is_array($old))
+ $old = explode("\n", $old);
+
+ if ($new === false)
+ $new = self::patch($old, $diff, true);
+
+ if (!is_array($new))
+ $new = explode("\n", $new);
+
+ $left = $right = $context = array();
+ $max_lines = max(count($new), count($old));
+
+ // Creating an array of changed lines for left and right texts
+ foreach ($diff as $line)
+ {
+ if (preg_match('!^(?P[0-9]+)(?:,(?P[0-9]+))?(?P[acd])(?P[0-9]+)(?:,(?[0-9]+))?$!', trim($line), $match))
+ {
+ if (empty($match['oe']))
+ $match['oe'] = $match['ob'];
+
+ if (empty($match['ne']))
+ $match['ne'] = $match['nb'];
+
+ if ($match['mode'] == 'a')
+ {
+ for ($i = $match['nb']; $i <= $match['ne']; $i++)
+ {
+ $right[$i - 1] = true;
+ $max_lines++;
+ }
+ }
+ elseif ($match['mode'] == 'd')
+ {
+ for ($i = $match['ob']; $i <= $match['oe']; $i++)
+ {
+ $left[$i - 1] = true;
+ $max_lines++;
+ }
+ }
+ else
+ {
+ for ($i = $match['nb']; $i <= $match['ne']; $i++)
+ {
+ $right[$i - 1] = true;
+ }
+ for ($i = $match['ob']; $i <= $match['oe']; $i++)
+ {
+ $left[$i - 1] = true;
+ }
+ }
+
+ if ($show_context && $show_context !== true)
+ {
+ $min = $match['ob'] - (int) $show_context;
+ if ($min < 1) $min = 1;
+ $max = $match['oe'] + (int) $show_context;
+ if ($max > count($new)) $max = count($new);
+
+ for ($i = $min; $i <= $max; $i++)
+ {
+ $context[$i - 1] = true;
+ }
+ }
+ }
+ }
+
+ $out = array();
+
+ $left_index = 0;
+ $right_index = 0;
+ $i = 0;
+
+ // Then we can compile this to an array of changed things
+ while ($i < $max_lines)
+ {
+ $row = array();
+
+ // Line present in left but not in right ? deleted
+ if (isset($left[$left_index]) && !isset($right[$right_index]))
+ {
+ $row = array(self::DEL, $old[$left_index], '');
+ $left_index++;
+ }
+ // Line present in right but not in left ? added
+ elseif (isset($right[$right_index]) && !isset($left[$left_index]))
+ {
+ $row = array(self::INS, '', $new[$right_index]);
+ $right_index++;
+ }
+ else
+ {
+ // Changed line
+ if (isset($left[$left_index]) && isset($right[$right_index]))
+ {
+ $row = array(self::CHANGED, $old[$left_index], $new[$right_index]);
+ }
+ // Or nothing happened
+ else
+ {
+ // We want all the context, ok
+ if ($show_context === true || isset($context[$left_index]))
+ {
+ $l = isset($old[$left_index]) ? $old[$left_index] : '';
+ $r = isset($new[$right_index]) ? $new[$right_index] : '';
+ $row = array(self::SAME, $l, $r);
+ }
+ }
+
+ $right_index++;
+ $left_index++;
+ }
+
+ $i++;
+
+ if (!empty($row))
+ {
+ $out[($i - 1)] = $row;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Generates a word-diff, like the GNU wdiff utility (kind of)
+ *
+ * @param string $old Left right to compare
+ * @param string $new Right line to compare
+ * @param string $union Union string to assemble words (default is whitespace)
+ */
+ static public function wdiff($old, $new, $union = ' ')
+ {
+ $diff = self::diff_to_array(false, explode(' ', $old), explode(' ', $new));
+ $out = '';
+
+ foreach ($diff as $line)
+ {
+ list ($change, $old, $new) = $line;
+
+ if ($change == self::CHANGED)
+ {
+ $out .= '[-' . $old . '-]';
+ $out .= $union;
+ $out .= '{+' . $new . '+}';
+ }
+ elseif ($change == self::DEL)
+ {
+ $out .= '[-' . $old . '-]';
+ }
+ elseif ($change == self::INS)
+ {
+ $out .= '{+' . $new . '+}';
+ }
+ else
+ {
+ $out .= $old;
+ }
+
+ $out .= $union;
+ }
+
+ $out = str_replace('+}' . $union . '{+', ' ', $out);
+ $out = str_replace('-]' . $union . '[-', ' ', $out);
+
+ return $out;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/garbage2xhtml/lib.garbage2xhtml.php b/include/libs/garbage2xhtml/lib.garbage2xhtml.php
new file mode 100644
index 0000000..28e06a0
--- /dev/null
+++ b/include/libs/garbage2xhtml/lib.garbage2xhtml.php
@@ -0,0 +1,868 @@
+.
+*/
+
+class Garbage_Exception extends Exception
+{
+}
+
+class garbage2xhtml
+{
+ /**
+ * Secure attributes contents?
+ * Will check for url scheme and url content in href and src
+ * It's advised to disable
+{/if}
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/template.build_dir.php b/include/libs/template_lite/internal/template.build_dir.php
new file mode 100644
index 0000000..365ce92
--- /dev/null
+++ b/include/libs/template_lite/internal/template.build_dir.php
@@ -0,0 +1,29 @@
+_get_dir($dir);
+ }
+ $_result = $object->_get_dir($dir);
+ foreach($_args as $value)
+ {
+ $_result .= $value;
+ if (!is_dir($_result))
+ {
+ mkdir($_result, 0777);
+ }
+ $_result.= DIRECTORY_SEPARATOR;
+ }
+ return $_result;
+}
+
+?>
diff --git a/include/libs/template_lite/internal/template.config_loader.php b/include/libs/template_lite/internal/template.config_loader.php
new file mode 100644
index 0000000..1da4338
--- /dev/null
+++ b/include/libs/template_lite/internal/template.config_loader.php
@@ -0,0 +1,76 @@
+_config_module_loaded = true;
+$this->template_dir = $this->_get_dir($this->template_dir);
+$this->config_dir = $this->_get_dir($this->config_dir);
+$this->compile_dir = $this->_get_dir($this->compile_dir);
+$name = ($this->encode_file_name) ? md5($this->template_dir . $file . $section_name . $var_name).'.php' : str_replace(".", "_", str_replace("/", "_", $file."_".$section_name."_".$var_name)).'.php';
+
+if ($this->debugging)
+{
+ $debug_start_time = array_sum(explode(' ', microtime()));
+}
+
+if ($this->cache)
+{
+ array_push($this->_cache_info['config'], $file);
+}
+
+if (!$this->force_compile && file_exists($this->compile_dir.'c_'.$name) && (filemtime($this->compile_dir.'c_'.$name) > filemtime($this->config_dir.$file)))
+{
+ include($this->compile_dir.'c_'.$name);
+ return true;
+}
+
+if (!is_object($this->_config_obj))
+{
+ require_once(TEMPLATE_LITE_DIR . "class.config.php");
+ $this->_config_obj = new $this->config_class;
+ $this->_config_obj->overwrite = $this->config_overwrite;
+ $this->_config_obj->booleanize = $this->config_booleanize;
+ $this->_config_obj->fix_new_lines = $this->config_fix_new_lines;
+ $this->_config_obj->read_hidden = $this->config_read_hidden;
+}
+
+if (!($_result = $this->_config_obj->config_load($this->config_dir.$file, $section_name, $var_name)))
+{
+ return false;
+}
+
+if (!empty($var_name) || !empty($section_name))
+{
+ $output = "\$this->_confs = " . var_export($_result, true) . ";";
+}
+else
+{
+ // must shift of the bottom level of the array to get rid of the section labels
+ $_temp = array();
+ foreach($_result as $value)
+ {
+ $_temp = array_merge($_temp, $value);
+ }
+ $output = "\$this->_confs = " . var_export($_temp, true) . ";";
+}
+
+$f = fopen($this->compile_dir.'c_'.$name, "w");
+fwrite($f, '');
+fclose($f);
+eval($output);
+
+if ($this->debugging)
+{
+ $this->_templatelite_debug_info[] = array('type' => 'config',
+ 'filename' => $file.' ['.$section_name.'] '.$var_name,
+ 'depth' => 0,
+ 'exec_time' => array_sum(explode(' ', microtime())) - $debug_start_time );
+}
+
+return true;
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/template.destroy_dir.php b/include/libs/template_lite/internal/template.destroy_dir.php
new file mode 100644
index 0000000..9538c28
--- /dev/null
+++ b/include/libs/template_lite/internal/template.destroy_dir.php
@@ -0,0 +1,72 @@
+template_dir = $object->_get_dir($object->template_dir);
+
+ $name = ($object->encode_file_name) ? md5($object->template_dir.$file).'.php' : str_replace(".", "_", str_replace("/", "_", $file)).'.php';
+ @unlink($dir.$name);
+ }
+ else
+ {
+ $_args = "";
+ foreach(explode('|', $id) as $value)
+ {
+ $_args .= $value.DIRECTORY_SEPARATOR;
+ }
+ template_rm_dir($dir.DIRECTORY_SEPARATOR.$_args);
+ }
+ }
+}
+
+function template_rm_dir($dir)
+{
+ if (is_file(substr($dir, 0, -1)))
+ {
+ @unlink(substr($dir, 0, -1));
+ return;
+ }
+
+ if (!file_exists($dir))
+ return;
+
+ if ($d = opendir($dir))
+ {
+ while(($f = readdir($d)) !== false)
+ {
+ if ($f != '.' && $f != '..')
+ {
+ template_rm_dir($dir.$f.DIRECTORY_SEPARATOR);
+ }
+ }
+ @rmdir($dir.$f);
+ }
+}
+
+?>
diff --git a/include/libs/template_lite/internal/template.fetch_compile_include.php b/include/libs/template_lite/internal/template.fetch_compile_include.php
new file mode 100644
index 0000000..491fe53
--- /dev/null
+++ b/include/libs/template_lite/internal/template.fetch_compile_include.php
@@ -0,0 +1,42 @@
+debugging)
+ {
+ $object->_templatelite_debug_info[] = array('type' => 'template',
+ 'filename' => $_templatelite_include_file,
+ 'depth' => ++$object->_inclusion_depth,
+ 'exec_time' => array_sum(explode(' ', microtime())) );
+ $included_tpls_idx = count($object->_templatelite_debug_info) - 1;
+ }
+
+ $object->_vars = array_merge($object->_vars, $_templatelite_include_vars);
+ $_templatelite_include_file = $object->_get_resource($_templatelite_include_file);
+ if(isset($object->_confs[0]))
+ {
+ array_unshift($object->_confs, $object->_confs[0]);
+ $_compiled_output = $object->_fetch_compile($_templatelite_include_file, true);
+ array_shift($object->_confs);
+ }
+ else
+ {
+ $_compiled_output = $object->_fetch_compile($_templatelite_include_file, true);
+ }
+
+ $object->_inclusion_depth--;
+
+ if ($object->debugging)
+ {
+ $object->_templatelite_debug_info[$included_tpls_idx]['exec_time'] = array_sum(explode(' ', microtime())) - $object->_templatelite_debug_info[$included_tpls_idx]['exec_time'];
+ }
+ return $_compiled_output;
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/internal/template.generate_debug_output.php b/include/libs/template_lite/internal/template.generate_debug_output.php
new file mode 100644
index 0000000..3214b57
--- /dev/null
+++ b/include/libs/template_lite/internal/template.generate_debug_output.php
@@ -0,0 +1,37 @@
+_vars;
+ ksort($assigned_vars);
+ if (@is_array($object->_config[0]))
+ {
+ $config_vars = $object->_config[0];
+ ksort($config_vars);
+ $object->assign("_debug_config_keys", array_keys($config_vars));
+ $object->assign("_debug_config_vals", array_values($config_vars));
+ }
+
+ $included_templates = $object->_templatelite_debug_info;
+
+ $object->assign("_debug_keys", array_keys($assigned_vars));
+ $object->assign("_debug_vals", array_values($assigned_vars));
+ $object->assign("_debug_tpls", $included_templates);
+ $object->assign("_templatelite_debug_output", "");
+
+ $object->_templatelite_debug_loop = true;
+ $object->_templatelite_debug_dir = $object->template_dir;
+ $object->template_dir = TEMPLATE_LITE_DIR . "internal/";
+ $debug_output = $object->fetch("debug.tpl");
+ $object->template_dir = $object->_templatelite_debug_dir;
+ $object->_templatelite_debug_loop = false;
+ return $debug_output;
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/block.capture.php b/include/libs/template_lite/plugins/block.capture.php
new file mode 100644
index 0000000..5ae6533
--- /dev/null
+++ b/include/libs/template_lite/plugins/block.capture.php
@@ -0,0 +1,29 @@
+_templatelite_vars['capture'][$buffer] = $content;
+ if (isset($assign))
+ {
+ $tpl->assign($assign, $content);
+ }
+ return;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/block.strip.php b/include/libs/template_lite/plugins/block.strip.php
new file mode 100644
index 0000000..c3d79a8
--- /dev/null
+++ b/include/libs/template_lite/plugins/block.strip.php
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/block.textformat.php b/include/libs/template_lite/plugins/block.textformat.php
new file mode 100644
index 0000000..da4fe74
--- /dev/null
+++ b/include/libs/template_lite/plugins/block.textformat.php
@@ -0,0 +1,78 @@
+ 0)
+ {
+ $paragraph = str_repeat($indent_char,$indent_first) . $paragraph;
+ }
+ // wordwrap sentences
+ $paragraph = wordwrap($paragraph, $wrap - $indent, $wrap_char, $wrap_cut);
+ // indent lines
+ if($indent > 0)
+ {
+ $paragraph = preg_replace('!^!m',str_repeat($indent_char,$indent),$paragraph);
+ }
+ $output .= $paragraph . $wrap_char . $wrap_char;
+ }
+ if($assign != null)
+ {
+ $template_object->assign($assign,$output);
+ }
+ else
+ {
+ echo $output;
+ }
+ //echo $content;
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/compiler.debug.php b/include/libs/template_lite/plugins/compiler.debug.php
new file mode 100644
index 0000000..efdfda9
--- /dev/null
+++ b/include/libs/template_lite/plugins/compiler.debug.php
@@ -0,0 +1,33 @@
+
+ * Purpose: popup debug window
+ * -------------------------------------------------------------
+ */
+function tpl_compiler_debug($params, &$tpl)
+{
+ if($params['output'])
+ {
+ $debug_output = '$this->assign("_templatelite_debug_output", ' . $params['output'] . ');';
+ }
+ else
+ {
+ $debug_output = "";
+ }
+
+ if(!function_exists("generate_compiler_debug_output"))
+ {
+ require_once(TEMPLATE_LITE_DIR . "internal/compile.generate_compiler_debug_output.php");
+ }
+ $debug_output .= generate_compiler_debug_output($tpl);
+ return $debug_output;
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/compiler.tplheader.php b/include/libs/template_lite/plugins/compiler.tplheader.php
new file mode 100644
index 0000000..bad0151
--- /dev/null
+++ b/include/libs/template_lite/plugins/compiler.tplheader.php
@@ -0,0 +1,16 @@
+_file . " compiled at " . date('Y-m-d H:M'). "';";
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.counter.php b/include/libs/template_lite/plugins/function.counter.php
new file mode 100644
index 0000000..a90c98e
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.counter.php
@@ -0,0 +1,97 @@
+assign($assign, $count[$name]);
+ }
+
+ if ($printval[$name])
+ {
+ $retval = $count[$name];
+ }
+ else
+ {
+ $retval = null;
+ }
+
+ if (isset($skip))
+ {
+ $skipval[$name] = $skip;
+ }
+ elseif (empty($skipval[$name]))
+ {
+ $skipval[$name] = 1;
+ }
+
+ if (isset($direction))
+ {
+ $dir[$name] = $direction;
+ }
+ elseif (!isset($dir[$name]))
+ {
+ $dir[$name] = "up";
+ }
+
+ if ($dir[$name] == "down")
+ {
+ $count[$name] -= $skipval[$name];
+ }
+ else
+ {
+ $count[$name] += $skipval[$name];
+ }
+
+ return $retval;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.cycle.php b/include/libs/template_lite/plugins/function.cycle.php
new file mode 100644
index 0000000..770b619
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.cycle.php
@@ -0,0 +1,101 @@
+
+ * Credits: Mark Priatel
+ * Gerard
+ * Jason Sweat
+ * Purpose: cycle through given values
+ * Input: name = name of cycle (optional)
+ * values = comma separated list of values to cycle,
+ * or an array of values to cycle
+ * (this can be left out for subsequent calls)
+ * reset = boolean - resets given var to true
+ * print = boolean - print var or not. default is true
+ * advance = boolean - whether or not to advance the cycle
+ * delimiter = the value delimiter, default is ","
+ * assign = boolean, assigns to template var instead of
+ * printed.
+ * Examples: {cycle values="#eeeeee,#d0d0d0d"}
+ * {cycle name=row values="one,two,three" reset=true}
+ * {cycle name=row}
+ * Credit: Taken from the original Smarty
+ * http://smarty.php.net
+ */
+function tpl_function_cycle($params, &$tpl)
+{
+ static $cycle_vars;
+
+ $name = (empty($params['name'])) ? 'default' : $params['name'];
+ $print = (isset($params['print'])) ? (bool)$params['print'] : true;
+ $advance = (isset($params['advance'])) ? (bool)$params['advance'] : true;
+ $reset = (isset($params['reset'])) ? (bool)$params['reset'] : false;
+
+ if (!in_array('values', array_keys($params)))
+ {
+ if(!isset($cycle_vars[$name]['values']))
+ {
+ throw new Template_Exception("cycle: missing 'values' parameter", $tpl);
+ return;
+ }
+ }
+ else
+ {
+ if(isset($cycle_vars[$name]['values']) && $cycle_vars[$name]['values'] != $params['values'] )
+ {
+ $cycle_vars[$name]['index'] = 0;
+ }
+ $cycle_vars[$name]['values'] = $params['values'];
+ }
+
+ $cycle_vars[$name]['delimiter'] = (isset($params['delimiter'])) ? $params['delimiter'] : ',';
+
+ if(is_array($cycle_vars[$name]['values']))
+ {
+ $cycle_array = $cycle_vars[$name]['values'];
+ }
+ else
+ {
+ $cycle_array = explode($cycle_vars[$name]['delimiter'],$cycle_vars[$name]['values']);
+ }
+
+ if(!isset($cycle_vars[$name]['index']) || $reset )
+ {
+ $cycle_vars[$name]['index'] = 0;
+ }
+
+ if (isset($params['assign']))
+ {
+ $print = false;
+ $tpl->assign($params['assign'], $cycle_array[$cycle_vars[$name]['index']]);
+ }
+
+ if($print)
+ {
+ $retval = $cycle_array[$cycle_vars[$name]['index']];
+ }
+ else
+ {
+ $retval = null;
+ }
+
+ if($advance)
+ {
+ if ( $cycle_vars[$name]['index'] >= count($cycle_array) -1 )
+ {
+ $cycle_vars[$name]['index'] = 0;
+ }
+ else
+ {
+ $cycle_vars[$name]['index']++;
+ }
+ }
+
+ return $retval;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.db_function_call.php b/include/libs/template_lite/plugins/function.db_function_call.php
new file mode 100644
index 0000000..2c61c38
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.db_function_call.php
@@ -0,0 +1,67 @@
+$db_function($params['db_query']);
+
+ $template_object->assign($params['db_assign'], $result);
+
+ if (!empty($params['db_errornumber_assign']))
+ {
+ $template_object->assign($params['db_errornumber_assign'], $db->ErrorNo());
+ }
+
+ if (!empty($params['db_error_assign']))
+ {
+ $template_object->assign($params['db_error_assign'], $db->ErrorMsg());
+ }
+
+ if (!empty($params['db_EOF_assign']))
+ {
+ $template_object->assign($params['db_EOF_assign'], $result->EOF);
+ }
+}
+?>
diff --git a/include/libs/template_lite/plugins/function.db_result_call.php b/include/libs/template_lite/plugins/function.db_result_call.php
new file mode 100644
index 0000000..5d166e0
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.db_result_call.php
@@ -0,0 +1,75 @@
+$db_function();
+
+ $template_object->assign($params['db_assign'], $result);
+
+ if (!empty($params['db_errornumber_assign']))
+ {
+ $template_object->assign($params['db_errornumber_assign'], $db->ErrorNo());
+ }
+
+ if (!empty($params['db_error_assign']))
+ {
+ $template_object->assign($params['db_error_assign'], $db->ErrorMsg());
+ }
+
+ if (!empty($params['db_EOF_assign']))
+ {
+ $template_object->assign($params['db_EOF_assign'], $result_object->EOF);
+ }
+}
+?>
diff --git a/include/libs/template_lite/plugins/function.html_checkboxes.php b/include/libs/template_lite/plugins/function.html_checkboxes.php
new file mode 100644
index 0000000..5a14a81
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_checkboxes.php
@@ -0,0 +1,70 @@
+
+ */
+function tpl_function_html_checkboxes($params, &$tpl)
+{
+ require_once("shared.escape_chars.php");
+ $name = null;
+ $value = null;
+ $checked = null;
+ $extra = '';
+
+ foreach($params as $_key => $_value)
+ {
+ switch($_key)
+ {
+ case 'name':
+ case 'value':
+ $$_key = $_value;
+ break;
+ case 'checked':
+ if ($_key == 'true' || $_key == 'yes' || $_key == 'on')
+ {
+ $$_key = true;
+ }
+ else
+ {
+ $$_key = false;
+ }
+ break;
+ default:
+ if(!is_array($_key))
+ {
+ $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';
+ }
+ else
+ {
+ throw new Template_Exception("html_checkbox: attribute '$_key' cannot be an array", $tpl);
+ }
+ }
+ }
+
+ if (!isset($name) || empty($name))
+ {
+ throw new Template_Exception("html_checkbox: missing 'name' parameter", $tpl);
+ return;
+ }
+
+ $toReturn = ' ';
+ return $toReturn;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_hidden.php b/include/libs/template_lite/plugins/function.html_hidden.php
new file mode 100644
index 0000000..7f53279
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_hidden.php
@@ -0,0 +1,49 @@
+
+ */
+function tpl_function_html_hidden($params, &$tpl)
+{
+ require_once("shared.escape_chars.php");
+ $name = null;
+ $value = '';
+ $extra = '';
+
+ foreach($params as $_key => $_value)
+ {
+ switch($_key)
+ {
+ case 'name':
+ case 'value':
+ $$_key = $_value;
+ break;
+ default:
+ if(!is_array($_key))
+ {
+ $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';
+ }
+ else
+ {
+ throw new Template_Exception("html_hidden: attribute '$_key' cannot be an array", $tpl);
+ }
+ }
+ }
+
+ if (!isset($name) || empty($name))
+ {
+ throw new Template_Exception("html_input: missing 'name' parameter", $tpl);
+ return;
+ }
+
+ $toReturn = ' ';
+ return $toReturn;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_image.php b/include/libs/template_lite/plugins/function.html_image.php
new file mode 100644
index 0000000..a3f3dd3
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_image.php
@@ -0,0 +1,203 @@
+
+ *
+ * {html_image url="http://www.yoursite.com/image.jpg"}
+ * {html_image url="images/me.gif" alt="A picture of me!"}
+ *
+ * Author: Paul Lockaby
+ */
+function tpl_function_html_image($params, &$tpl)
+{
+ require_once("shared.escape_chars.php");
+ $alt = '';
+ $url = '';
+ $height = '';
+ $width = '';
+ $extra = '';
+ $prefix = '';
+ $suffix = '';
+ $basedir = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '';
+ foreach($params as $_key => $_val)
+ {
+ switch($_key)
+ {
+ case 'url':
+ case 'height':
+ case 'width':
+ $$_key = $_val;
+ break;
+ case 'limit':
+ $$_key = true;
+ break;
+ case 'alt':
+ if(!is_array($_val))
+ {
+ $$_key = tpl_escape_chars($_val);
+ }
+ else
+ {
+ throw new Template_Exception("html_image: attribute '$_key' cannot be an array", $tpl);
+ }
+ break;
+ case 'link':
+ case 'href':
+ $prefix = '';
+ $suffix = ' ';
+ break;
+ default:
+ if(!is_array($_val))
+ {
+ $extra .= ' '.$_key.'="'.template_function_escape_special_chars($_val).'"';
+ }
+ else
+ {
+ throw new Template_Exception("html_image: extra attribute '$_key' cannot be an array", $tpl);
+ }
+ break;
+ }
+ }
+
+ if (empty($url))
+ {
+ throw new Template_Exception("html_image: missing 'url' parameter", $tpl);
+ return;
+ }
+
+ if (substr($file,0,1) == '/')
+ {
+ $_image_path = $basedir . $file;
+ }
+ else
+ {
+ $_image_path = $file;
+ }
+
+ // 0 = width, 1 = height
+ if ($size = @getimagesize($url))
+ {
+ if (empty($limit) || $limit == false)
+ {
+ // only a height was specified; we will fill in the width
+ if (!empty($height))
+ {
+ $width = $size[0];
+ }
+ // only a width was specified; we will fill in the height
+ if (!empty($width))
+ {
+ $height = $size[1];
+ }
+ // neither a height nor a width was specified; we will fill in both
+ if (empty($width) && empty($height))
+ {
+ $width = $size[0];
+ $height = $size[1];
+ }
+ }
+ else
+ {
+ if ((!empty($width) && ($size[0] > $width)) || (!empty($height) && ($size[1] > $height)))
+ {
+ if (!empty($height) && !empty($width))
+ {
+ // compare the ratios to determine how much each dimension needs to be changed
+
+ // this will return the width if the height is set to specified
+ $bth_width = round($size[0]*($height/$size[1]));
+
+ // this will return the height if the width is set to specified
+ $bth_height = round($size[1]*($width/$size[0]));
+
+ // first we set the width to the max and see how big the height will be
+ if (!($bth_height > $height))
+ {
+ // returned height is acceptable (i.e. less than specified)
+ $fin1_height = $bth_height;
+ $fin1_width = $width;
+ }
+
+ // now we set the height to the max and see how big the width will be
+ if (!($bth_width > $width))
+ {
+ // returned width is acceptable (i.e. less than specified)
+ $fin2_height = $height;
+ $fin2_width = $bth_width;
+ }
+
+ // check to see if both of them went through
+ if (isset($fin1_height) && isset($fin1_width) && isset($fin2_height) && isset($fin2_width))
+ {
+ // now check the difference between abs($fin1_height-$fin1_width) and abs($fin2_height-$fin2_width)
+ // since we obviously want the larger image, take whichever one has the smaller difference
+ if (abs($fin1_height - $fin1_width) < abs($fin2_height - $fin2_width))
+ {
+ $new_height = $fin1_height;
+ $new_width = $fin1_width;
+ }
+ else
+ {
+ $new_height = $fin2_height;
+ $new_width = $fin2_width;
+ }
+ }
+ else
+ {
+ // since $new_height and $new_width weren't set above, we have to set them here
+ if (isset($fin1_height) && isset($fin1_width))
+ {
+ $new_height = $fin1_height;
+ $new_width = $fin1_width;
+ }
+ else
+ {
+ $new_height = $fin2_height;
+ $new_width = $fin2_width;
+ }
+ }
+ }
+ else
+ {
+ // only a height or only a width was specified
+ // much easier
+ if (!empty($height))
+ {
+ // working with only a height now
+ $new_height = $height;
+ $new_width = round($size[0]*($height/$size[1]));
+ }
+ if (!empty($width))
+ {
+ // working with only a width now
+ $new_height = round($size[1]*($width/$size[0]));
+ $new_width = $width;
+ }
+ }
+ $width = $new_width;
+ $height = $new_height;
+ }
+ else
+ {
+ $width = $size[0];
+ $height = $size[1];
+ }
+ }
+ }
+
+ return $prefix . ' ' . $suffix;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_input.php b/include/libs/template_lite/plugins/function.html_input.php
new file mode 100644
index 0000000..453554d
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_input.php
@@ -0,0 +1,58 @@
+
+ */
+function tpl_function_html_input($params, &$tpl)
+{
+ require_once("shared.escape_chars.php");
+ $name = null;
+ $value = '';
+ $password = false;
+ $extra = '';
+
+ foreach($params as $_key => $_value)
+ {
+ switch($_key)
+ {
+ case 'name':
+ case 'value':
+ $$_key = $_value;
+ break;
+ case 'password':
+ $$_key = true;
+ break;
+ default:
+ if(!is_array($_key))
+ {
+ $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';
+ }
+ else
+ {
+ throw new Template_Exception("html_input: attribute '$_key' cannot be an array", $tpl);
+ }
+ }
+ }
+
+ if (!isset($name) || empty($name))
+ {
+ throw new Template_Exception("html_input: missing 'name' parameter", $tpl);
+ return;
+ }
+
+ $toReturn = ' ';
+ return $toReturn;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_options.php b/include/libs/template_lite/plugins/function.html_options.php
new file mode 100644
index 0000000..b19f097
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_options.php
@@ -0,0 +1,104 @@
+ $_val)
+ {
+ switch($_key)
+ {
+ case 'name':
+ $$_key = (string)$_val;
+ break;
+ case 'options':
+ $$_key = (array)$_val;
+ break;
+ case 'values':
+ case 'output':
+ $$_key = array_values((array)$_val);
+ break;
+ case 'selected':
+ $$_key = array_values((array)$_val);
+ break;
+ default:
+ if(!is_array($_key))
+ {
+ $extra .= ' ' . $_key . '="' . tpl_escape_chars($_val) . '"';
+ }
+ else
+ {
+ throw new Template_Exception("html_select: attribute '$_key' cannot be an array", $tpl);
+ }
+ break;
+ }
+ }
+
+ $_html_result = '';
+ if (is_array($options))
+ {
+ foreach ($options as $_key=>$_val)
+ {
+ $_html_result .= tpl_function_html_options_optoutput($tpl, $_key, $_val, $selected);
+ }
+ }
+ else
+ {
+ foreach ((array)$values as $_i=>$_key)
+ {
+ $_val = isset($output[$_i]) ? $output[$_i] : '';
+ $_html_result .= tpl_function_html_options_optoutput($tpl, $_key, $_val, $selected);
+ }
+ }
+
+ if(!empty($name))
+ {
+ $_html_result = '' . "\n" . $_html_result . ' ' . "\n";
+ }
+
+ return $_html_result;
+}
+
+function tpl_function_html_options_optoutput(&$tpl, $key, $value, $selected)
+{
+ if(!is_array($value))
+ {
+ $_html_result = '' . "\n";
+ }
+ else
+ {
+ $_html_result = tpl_function_html_options_optgroup($tpl, $key, $value, $selected);
+ }
+ return $_html_result;
+}
+
+function tpl_function_html_options_optgroup(&$tpl, $key, $values, $selected)
+{
+ $optgroup_html = ' ' . "\n";
+ foreach ($values as $key => $value)
+ {
+ $optgroup_html .= tpl_function_html_options_optoutput($tpl, $key, $value, $selected);
+ }
+ $optgroup_html .= " \n";
+ return $optgroup_html;
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_radios.php b/include/libs/template_lite/plugins/function.html_radios.php
new file mode 100644
index 0000000..7b2f236
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_radios.php
@@ -0,0 +1,55 @@
+
+ */
+function tpl_function_html_radios($params, &$tpl)
+{
+ require_once("shared.escape_chars.php");
+ $name = null;
+ $value = '';
+ $extra = '';
+
+ foreach($params as $_key => $_value)
+ {
+ switch($_key)
+ {
+ case 'name':
+ case 'value':
+ $$_key = $_value;
+ break;
+ default:
+ if(!is_array($_key))
+ {
+ $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';
+ }
+ else
+ {
+ throw new Template_Exception("html_radio: attribute '$_key' cannot be an array", $tpl);
+ }
+ }
+ }
+
+ if (!isset($name) || empty($name))
+ {
+ throw new Template_Exception("html_radio: missing 'name' parameter", $tpl);
+ return;
+ }
+
+ $toReturn = ' ';
+ return $toReturn;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.html_select_date.php b/include/libs/template_lite/plugins/function.html_select_date.php
new file mode 100644
index 0000000..c93eb2b
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_select_date.php
@@ -0,0 +1,269 @@
+'s of the different tags.
+ If not set, uses default dropdown. */
+ $day_size = null;
+ $month_size = null;
+ $year_size = null;
+ /* Unparsed attributes common to *ALL* the / tags.
+ An example might be in the template: all_extra ='class ="foo"'. */
+ $all_extra = null;
+ /* Separate attributes for the tags. */
+ $day_extra = null;
+ $month_extra = null;
+ $year_extra = null;
+ /* Order in which to display the fields.
+ "D" -> day, "M" -> month, "Y" -> year. */
+ $field_order = 'MDY';
+ /* String printed between the different fields. */
+ $field_separator = "\n";
+ $time = time();
+
+ extract($params);
+
+ // If $time is not in format yyyy-mm-dd
+ if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $time))
+ {
+ // then $time is empty or unix timestamp or mysql timestamp
+ // using smarty_make_timestamp to get an unix timestamp and
+ // strftime to make yyyy-mm-dd
+ $time = strftime('%Y-%m-%d', tpl_make_timestamp($time));
+ }
+ // Now split this in pieces, which later can be used to set the select
+ $time = explode("-", $time);
+
+ // make syntax "+N" or "-N" work with start_year and end_year
+ if (preg_match('!^(\+|\-)\s*(\d+)$!', $end_year, $match))
+ {
+ if ($match[1] == '+')
+ {
+ $end_year = strftime('%Y') + $match[2];
+ }
+ else
+ {
+ $end_year = strftime('%Y') - $match[2];
+ }
+ }
+ if (preg_match('!^(\+|\-)\s*(\d+)$!', $start_year, $match))
+ {
+ if ($match[1] == '+')
+ {
+ $start_year = strftime('%Y') + $match[2];
+ }
+ else
+ {
+ $start_year = strftime('%Y') - $match[2];
+ }
+ }
+
+ $field_order = strtoupper($field_order);
+ $html_result = $month_result = $day_result = $year_result = "";
+
+ if ($display_months)
+ {
+ $month_names = array();
+ $month_values = array();
+
+ for ($i = 1; $i <= 12; $i++)
+ {
+ $month_names[] = strftime($month_format, mktime(0, 0, 0, $i, 1, 2000));
+ $month_values[] = strftime($month_value_format, mktime(0, 0, 0, $i, 1, 2000));
+ }
+
+ $month_result .= ' $month_names,
+ 'values' => $month_values,
+ 'selected' => $month_values[$time[1]-1],
+ 'print_result' => false),
+ $template_object);
+ $month_result .= ' ';
+ }
+
+ if ($display_days)
+ {
+ $days = array();
+ for ($i = 1; $i <= 31; $i++)
+ {
+ $days[] = sprintf($day_format, $i);
+ $day_values[] = sprintf($day_value_format, $i);
+ }
+
+ $day_result .= ' $days,
+ 'values' => $day_values,
+ 'selected' => $time[2],
+ 'print_result' => false),
+ $template_object);
+ $day_result .= ' ';
+ }
+
+ if ($display_years)
+ {
+ if (null !== $field_array)
+ {
+ $year_name = $field_array . '[' . $prefix . 'Year]';
+ }
+ else
+ {
+ $year_name = $prefix . 'Year';
+ }
+ if ($year_as_text)
+ {
+ $year_result .= ' $years,
+ 'values' => $years,
+ 'selected' => $time[0],
+ 'print_result' => false),
+ $template_object);
+ $year_result .= ' ';
+ }
+ }
+
+ // Loop thru the field_order field
+ for ($i = 0; $i <= 2; $i++)
+ {
+ $c = substr($field_order, $i, 1);
+ switch ($c)
+ {
+ case 'D':
+ $html_result .= $day_result;
+ break;
+
+ case 'M':
+ $html_result .= $month_result;
+ break;
+
+ case 'Y':
+ $html_result .= $year_result;
+ break;
+ }
+ // Add the field seperator
+ if($i != 2)
+ {
+ $html_result .= $field_separator;
+ }
+ }
+ return $html_result;
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/function.html_select_time.php b/include/libs/template_lite/plugins/function.html_select_time.php
new file mode 100644
index 0000000..48c6b8f
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_select_time.php
@@ -0,0 +1,177 @@
+'."\n";
+ $html_result .= tpl_function_html_options(array('output' => $hours,
+ 'values' => $hours,
+ 'selected' => strftime($hour_fmt, $time),
+ 'print_result' => false),
+ $template_object);
+ $html_result .= " \n";
+ }
+
+ if ($display_minutes)
+ {
+ $all_minutes = range(0, 59);
+ for ($i = 0, $for_max = count($all_minutes); $i < $for_max; $i+= $minute_interval)
+ {
+ $minutes[] = sprintf('%02d', $all_minutes[$i]);
+ }
+ $selected = intval(floor(strftime('%M', $time) / $minute_interval) * $minute_interval);
+ $html_result .= ' $minutes,
+ 'values' => $minutes,
+ 'selected' => $selected,
+ 'print_result' => false),
+ $template_object);
+ $html_result .= " \n";
+ }
+
+ if ($display_seconds)
+ {
+ $all_seconds = range(0, 59);
+ for ($i = 0, $for_max = count($all_seconds); $i < $for_max; $i+= $second_interval)
+ {
+ $seconds[] = sprintf('%02d', $all_seconds[$i]);
+ }
+ $selected = intval(floor(strftime('%S', $time) / $second_interval) * $second_interval);
+ $html_result .= ' $seconds,
+ 'values' => $seconds,
+ 'selected' => $selected,
+ 'print_result' => false),
+ $template_object);
+ $html_result .= " \n";
+ }
+
+ if ($display_meridian && !$use_24_hours)
+ {
+ $html_result .= ' array('AM', 'PM'),
+ 'values' => array('am', 'pm'),
+ 'selected' => strtolower(strftime('%p', $time)),
+ 'print_result' => false),
+ $template_object);
+ $html_result .= " \n";
+ }
+ return $html_result;
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/function.html_table.php b/include/libs/template_lite/plugins/function.html_table.php
new file mode 100644
index 0000000..1b04dd7
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_table.php
@@ -0,0 +1,88 @@
+
+ * Purpose: make an html table from an array of data
+ * Input: loop = array to loop through
+ * cols = number of columns
+ * table_attr = table attributes
+ * tr_attr = table row attributes (arrays are cycled)
+ * td_attr = table cell attributes (arrays are cycled)
+ * trailpad = value to pad trailing cells with
+ *
+ * Examples: {table loop=$data}
+ * {$table loop=$data cols=4 tr_attr='"bgcolor=red"'}
+ * {$table loop=$data cols=4 tr_attr=$colors}
+ * Taken from the original Smarty
+ * http://smarty.php.net
+ * -------------------------------------------------------------
+ */
+function tpl_function_html_table($params, &$template_object)
+{
+ $table_attr = 'border="1"';
+ $tr_attr = '';
+ $td_attr = '';
+ $cols = 3;
+ $trailpad = ' ';
+
+ extract($params);
+
+ if (!isset($loop))
+ {
+ throw new Template_Exception("html_table: missing 'loop' parameter", $template_object);
+ return;
+ }
+
+ $output = "\n";
+ $output .= "\n";
+
+ for($x = 0, $y = count($loop); $x < $y; $x++)
+ {
+ $output .= "" . $loop[$x] . " \n";
+ if((!(($x+1) % $cols)) && $x < $y-1)
+ {
+ // go to next row
+ $output .= " \n\n";
+ }
+ if($x == $y-1)
+ {
+ // last row, pad remaining cells
+ $cells = $cols - $y % $cols;
+ if($cells != $cols) {
+ for($padloop = 0; $padloop < $cells; $padloop++) {
+ $output .= "$trailpad \n";
+ }
+ }
+ $output .= " \n";
+ }
+ }
+ $output .= "
\n";
+ return $output;
+}
+
+function tpl_function_html_table_cycle($name, $var)
+{
+ static $names = array();
+
+ if(!is_array($var))
+ {
+ return $var;
+ }
+
+ if(!isset($names[$name]) || $names[$name] == count($var)-1)
+ {
+ $names[$name] = 0;
+ return $var[0];
+ }
+
+ $names[$name]++;
+ return $var[$names[$name]];
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/function.html_textbox.php b/include/libs/template_lite/plugins/function.html_textbox.php
new file mode 100644
index 0000000..da6dbd4
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.html_textbox.php
@@ -0,0 +1,51 @@
+
+ */
+function tpl_function_html_textbox($params, &$tpl)
+{
+ require_once("shared.escape_chars.php");
+ $name = null;
+ $value = '';
+ $extra = '';
+
+ foreach($params as $_key => $_value)
+ {
+ switch($_key)
+ {
+ case 'name':
+ case 'value':
+ $$_key = $_value;
+ break;
+ default:
+ if(!is_array($_key))
+ {
+ $extra .= ' ' . $_key . '="' . tpl_escape_chars($_value) . '"';
+ }
+ else
+ {
+ throw new Template_Exception("html_textbox: attribute '$_key' cannot be an array", $tpl);
+ }
+ }
+ }
+
+ if (!isset($name) || empty($name))
+ {
+ throw new Template_Exception("html_textbox: missing 'name' parameter", $tpl);
+ return;
+ }
+
+ $toReturn = '';
+ return $toReturn;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.in_array.php b/include/libs/template_lite/plugins/function.in_array.php
new file mode 100644
index 0000000..a37aa9e
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.in_array.php
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/function.mailto.php b/include/libs/template_lite/plugins/function.mailto.php
new file mode 100644
index 0000000..af9273f
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.mailto.php
@@ -0,0 +1,148 @@
+
+ * Credits: Jason Sweat (added cc, bcc and subject functionality)
+ * Purpose: automate mailto address link creation, and optionally
+ * encode them.
+ * Input: address = e-mail address
+ * text = (optional) text to display, default is address
+ * encode = (optional) can be one of:
+ * none : no encoding (default)
+ * javascript : encode with javascript
+ * hex : encode with hexidecimal (no javascript)
+ * cc = (optional) address(es) to carbon copy
+ * bcc = (optional) address(es) to blind carbon copy
+ * subject = (optional) e-mail subject
+ * newsgroups = (optional) newsgroup(s) to post to
+ * followupto = (optional) address(es) to follow up to
+ * extra = (optional) extra tags for the href link
+ *
+ * Examples: {mailto address="me@domain.com"}
+ * {mailto address="me@domain.com" encode="javascript"}
+ * {mailto address="me@domain.com" encode="hex"}
+ * {mailto address="me@domain.com" subject="Hello to you!"}
+ * {mailto address="me@domain.com" cc="you@domain.com,they@domain.com"}
+ * {mailto address="me@domain.com" extra='class="mailto"'}
+ * Taken from the original Smarty
+ * http://smarty.php.net
+ * -------------------------------------------------------------
+ */
+function tpl_function_mailto($params, &$template_object)
+{
+ extract($params);
+
+ if (empty($address))
+ {
+ throw new Template_Exception("mailto: missing 'address' parameter", $template_object);
+ return;
+ }
+
+ if (empty($text))
+ {
+ $text = $address;
+ }
+
+ if (empty($extra))
+ {
+ $extra = "";
+ }
+
+ // netscape and mozilla do not decode %40 (@) in BCC field (bug?)
+ // so, don't encode it.
+
+ $mail_parms = array();
+ if (!empty($cc))
+ {
+ $mail_parms[] = 'cc='.str_replace('%40','@',rawurlencode($cc));
+ }
+
+ if (!empty($bcc))
+ {
+ $mail_parms[] = 'bcc='.str_replace('%40','@',rawurlencode($bcc));
+ }
+
+ if (!empty($subject))
+ {
+ $mail_parms[] = 'subject='.rawurlencode($subject);
+ }
+
+ if (!empty($newsgroups))
+ {
+ $mail_parms[] = 'newsgroups='.rawurlencode($newsgroups);
+ }
+
+ if (!empty($followupto))
+ {
+ $mail_parms[] = 'followupto='.str_replace('%40','@',rawurlencode($followupto));
+ }
+
+ $mail_parm_vals = "";
+ for ($i=0; $i'.$text.' \');';
+ $js_encode = '';
+ for ($x=0; $x < strlen($string); $x++)
+ {
+ $js_encode .= '%' . bin2hex($string[$x]);
+ }
+ return '';
+ }
+ elseif ($encode == 'hex')
+ {
+ preg_match('!^(.*)(\?.*)$!',$address,$match);
+ if(!empty($match[2]))
+ {
+ throw new Template_Exception("mailto: hex encoding does not work with extra attributes. Try javascript.", $template_object);
+ return;
+ }
+ $address_encode = "";
+ for ($x=0; $x < strlen($address); $x++)
+ {
+ if(preg_match('!\w!',$address[$x]))
+ {
+ $address_encode .= '%' . bin2hex($address[$x]);
+ }
+ else
+ {
+ $address_encode .= $address[$x];
+ }
+ }
+ $text_encode = "";
+ for ($x=0; $x < strlen($text); $x++)
+ {
+ $text_encode .= '' . bin2hex($text[$x]).';';
+ }
+ return ''.$text_encode.' ';
+ }
+ else
+ {
+ // no encoding
+ return ''.$text.' ';
+ }
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/function.math.php b/include/libs/template_lite/plugins/function.math.php
new file mode 100644
index 0000000..7369ef7
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.math.php
@@ -0,0 +1,90 @@
+ $val)
+ {
+ if ($key != "equation" && $key != "format" && $key != "assign")
+ {
+ // make sure value is not empty
+ if (strlen($val)==0)
+ {
+ throw new Template_Exception("math: parameter $key is empty", $template_object);
+ return;
+ }
+ if (!is_numeric($val))
+ {
+ throw new Template_Exception("math: parameter $key: is not numeric", $template_object);
+ return;
+ }
+ $equation = preg_replace("/\b$key\b/",$val, $equation);
+ }
+ }
+
+ eval("\$template_object_math_result = ".$equation.";");
+
+ if (empty($params['format']))
+ {
+ if (empty($params['assign']))
+ {
+ return $template_object_math_result;
+ }
+ else
+ {
+ $template_object->assign($params['assign'],$template_object_math_result);
+ }
+ }
+ else
+ {
+ if (empty($params['assign']))
+ {
+ printf($params['format'],$template_object_math_result);
+ }
+ else
+ {
+ $template_object->assign($params['assign'],sprintf($params['format'],$template_object_math_result));
+ }
+ }
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/function.popup.php b/include/libs/template_lite/plugins/function.popup.php
new file mode 100644
index 0000000..3f68fcc
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.popup.php
@@ -0,0 +1,81 @@
+
diff --git a/include/libs/template_lite/plugins/function.popup_init.php b/include/libs/template_lite/plugins/function.popup_init.php
new file mode 100644
index 0000000..bbcd2b7
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.popup_init.php
@@ -0,0 +1,32 @@
+' . "\n"
+ . '' . "\n";
+ }
+ else
+ {
+ throw new Template_Exception("popup_init: missing src parameter", $template_object);
+ }
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/function.resize_image.php b/include/libs/template_lite/plugins/function.resize_image.php
new file mode 100644
index 0000000..206a0df
--- /dev/null
+++ b/include/libs/template_lite/plugins/function.resize_image.php
@@ -0,0 +1,239 @@
+/**
+ * Template-Lite {resize_image} function plugin Using Image Magic
+ *
+ * Type: function
+ * Name: resize_image
+ * Purpose: Outputs resized image based on values passed.
+ * Input:
+ * - img_src tag path
+ * - directory = full directory path where images are located
+ * - thumbdir = Optional directory path to store thumbnail images. directory will be used if not supplied.
+ * - filename = Name of the file
+ * - xscale = Image max width size default 2000 px
+ * - yscale = Image max height size default 2000 px
+ * - thumbname = prefix name for new image, the thumb name is not needed if you are using a return type of 1
+ * - returntype = 1 - return image tag with full size image name and path but with height and width attributes adjusted
+ * 0 - resize image and store thumbnail in directory and return that thumbnail img tag. Default setting
+ * - url = Optional URL to download image from for resizing if the image doesn't exist in the image directory (CURL must be installed to use this feature)
+ * - binpath = Optional path to the ImageMagick mogrify command. If missing slower GD code will be used.
+ * - alt = Optional alt attribute for the img tag Default is "image"
+ * - border = Optional border attribute for img tag Default is 0
+ * - class = Optional class attribute for img tag
+ * - daystokeep = Optional number of days to cache the thumbnail image. Default is 5 days
+ *
+ * Examples:
+ *
+ * {resize_image img_src="/thumbnails/" directory="/html/mysite/ad_images/" thumbdir="/html/mysite/thumbnails/" filename="Myfile.jpg" xscale="150" yscale="200" thumbname="thumb_"}
+ *
+ *
+ * Output
+ *
+ * Author: Rick Thomson rick@oznet.com
+ * Author: Mark Dickenson akapanamajack@sourceforge.net
+*/
+
+// Calculate percentage between width and height
+function tpl_function_resize_percent($maximum, $current)
+{
+ return (real)(100 * ($maximum / $current));
+}
+
+function tpl_function_resize_unpercent($percent, $whole)
+{
+ return (real)(($percent * $whole) / 100);
+}
+
+function tpl_function_resize_image($params, &$tpl)
+{
+ extract($params);
+
+ if (empty($directory))
+ {
+ throw new Template_Exception("resize_image: missing 'directory' parameter", $tpl);
+ }
+
+ if (empty($thumbdir))
+ {
+ $thumbdir = $directory;
+ }
+
+ if (empty($filename))
+ {
+ throw new Template_Exception("resize_image: missing 'filename' parameter", $tpl);
+ }
+
+ if (empty($xscale))
+ {
+ $xscale = 2000;
+ }
+ $maximagewidth=$xscale;
+
+ if (empty($xscale))
+ {
+ $yscale = 2000;
+ }
+ $maximageheight=$yscale;
+
+ if (empty($alt))
+ {
+ $alt = "image";
+ }
+
+ if (empty($border))
+ {
+ $border = 0;
+ }
+
+ if (empty($daystokeep))
+ {
+ $daystokeep = 5;
+ }
+
+ if (!function_exists('gd_info'))
+ {
+ throw new Template_Exception("resize_image: the GD library is not installed", $tpl);
+ }
+
+ if(!file_exists($directory . $filename) && !empty($url) && function_exists('curl_init'))
+ {
+ $ch = curl_init ($url . $filename);
+ $fp = fopen ($directory . $filename, "w");
+ curl_setopt ($ch, CURLOPT_FILE, $fp);
+ curl_setopt ($ch, CURLOPT_HEADER, 0);
+ curl_exec ($ch);
+ curl_close ($ch);
+ fclose ($fp);
+ }
+
+ if(file_exists($directory . $filename))
+ {
+ $imageinfo = @getimagesize($directory . $filename);
+ if(empty($imageinfo))
+ {
+ return;
+ }
+
+ if ($returntype == 1)
+ {
+ $imagewidth = $imageinfo[0];
+ $imageheight = $imageinfo[1];
+
+ if($maximagewidth < $imagewidth) {
+ $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight);
+ $imagewidth = $maximagewidth;
+ }
+
+ if($maximageheight < $imageheight) {
+ $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth);
+ $imageheight = $maximageheight;
+ }
+ return " ";
+ }
+
+ if (empty($thumbname))
+ {
+ throw new Template_Exception("resize_image: missing 'thumbname' parameter", $tpl);
+ }
+
+ $now=urlencode(date("F j, Y, g:i a"));
+
+ $newimagepath = $thumbdir . $thumbname . $filename;
+
+ $newthumbnail = 0;
+ if(!file_exists($newimagepath))
+ {
+ copy($directory . $filename, $newimagepath);
+ $newthumbnail = 1;
+ }
+
+ $datechanged = date("j", time()) - date("j", filemtime($newimagepath));
+ if(($datechanged > -$daystokeep && $datechanged < $daystokeep) && $newthumbnail = 0)
+ {
+ // Do not rebuild
+ $imagewidth = $imageinfo[0];
+ $imageheight = $imageinfo[1];
+
+ if($maximagewidth < $imagewidth)
+ {
+ $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight);
+ $imagewidth = $maximagewidth;
+ }
+
+ if($maximageheight < $imageheight)
+ {
+ $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth);
+ $imageheight = $maximageheight;
+ }
+ }
+ else
+ {
+ // rebuild
+ copy($directory . $filename, $newimagepath);
+
+ $imagewidth = $imageinfo[0];
+ $imageheight = $imageinfo[1];
+
+ if($maximagewidth < $imagewidth)
+ {
+ $imageheight = tpl_function_resize_unpercent(tpl_function_resize_percent($maximagewidth, $imagewidth), $imageheight);
+ $imagewidth = $maximagewidth;
+ }
+
+ if($maximageheight < $imageheight)
+ {
+ $imagewidth = tpl_function_resize_unpercent(tpl_function_resize_percent($maximageheight, $imageheight), $imagewidth);
+ $imageheight = $maximageheight;
+ }
+ $imagewidth = round($imagewidth);
+ $imageheight = round($imageheight);
+ $scale = $imagewidth . "x" . $imageheight . "!";
+
+ if (empty($binpath))
+ {
+ if($imageinfo[2] == 1)
+ {
+ $sourceimage = imagecreatefromgif($directory . $filename);
+ }
+ elseif($imageinfo[2] == 2)
+ {
+ $sourceimage = imagecreatefromjpeg($directory . $filename);
+ }
+ elseif($imageinfo[2] == 3)
+ {
+ $sourceimage = imagecreatefrompng($directory . $filename);
+ }
+
+ $destinationimage = imagecreatetruecolor($imagewidth, $imageheight);
+ imagecopyresized($destinationimage, $sourceimage, 0, 0, 0, 0, $imagewidth, $imageheight, $imageinfo[0], $imageinfo[1]);
+ if($imageinfo[2] == 1)
+ {
+ imagegif($destinationimage, $newimagepath);
+ }
+ elseif($imageinfo[2] == 2)
+ {
+ imageJPEG($destinationimage, $newimagepath, 75);
+ }
+ elseif($imageinfo[2] == 3)
+ {
+ imagepng($destinationimage, $newimagepath);
+ }
+ imagedestroy($sourceimage);
+ imagedestroy($destinationimage);
+ }
+ else
+ {
+ if ($imageinfo[2] == 2)
+ {
+ system( $binpath . "mogrify -quality 75 -geometry $scale $newimagepath");
+ }
+ else
+ {
+ system( $binpath . "mogrify -geometry $scale $newimagepath");
+ }
+ }
+ }
+
+ return " ";
+ }
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.bbcode2html.php b/include/libs/template_lite/plugins/modifier.bbcode2html.php
new file mode 100644
index 0000000..b1c6f76
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.bbcode2html.php
@@ -0,0 +1,44 @@
+
+ * - string: data to convert
+ */
+function tpl_modifier_bbcode2html($data)
+{
+ $data = nl2br(stripslashes(addslashes($data)));
+
+ $search = array("\n", "\r", "[b]", "[/b]", "[i]", "[/i]", "[u]", "[/u]");
+ $replace = array("", "", "", " ", "", " ", "", " ");
+ $data = str_replace($search, $replace, $data);
+
+ $search = array(
+ "/\[email\](.*?)\[\/email\]/si",
+ "/\[email=(.*?)\](.*?)\[\/email\]/si",
+ "/\[url\](.*?)\[\/url\]/si",
+ "/\[url=(.*?)\](.*?)\[\/url\]/si",
+ "/\[img\](.*?)\[\/img\]/si",
+ "/\[code\](.*?)\[\/code\]/si",
+ "/\[pre\](.*?)\[\/pre\]/si",
+ "/\[list\](.*?)\[\/list\]/si",
+ "/\[\*\](.*?)/si"
+ );
+ $replace = array(
+ "\\1 ",
+ "\\2 ",
+ "\\1 ",
+ "\\2 ",
+ " ",
+ "
code: \\1
",
+ "\\1 ",
+ "",
+ "\\1 "
+ );
+ $data = preg_replace($search, $replace, $data);
+ return $data;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.capitalize.php b/include/libs/template_lite/plugins/modifier.capitalize.php
new file mode 100644
index 0000000..8797d8c
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.capitalize.php
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.cat.php b/include/libs/template_lite/plugins/modifier.cat.php
new file mode 100644
index 0000000..ee655c5
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.cat.php
@@ -0,0 +1,31 @@
+
+ * Name: cat
+ * Date: Feb 24, 2003
+ * Purpose: catenate a value to a variable
+ * Input: string to catenate
+ * Example: {$var|cat:"foo"}
+ * @link http://smarty.php.net/manual/en/language.modifier.cat.php cat
+ * (Smarty online manual)
+ * @author Monte Ohrt
+ * @version 1.0
+ * @param string
+ * @param string
+ * @return string
+ */
+function tpl_modifier_cat($string, $cat)
+{
+ return $string . $cat;
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.count_characters.php b/include/libs/template_lite/plugins/modifier.count_characters.php
new file mode 100644
index 0000000..56e98d3
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.count_characters.php
@@ -0,0 +1,32 @@
+
+ * Name: count_characteres
+ * Purpose: count the number of characters in a text
+ * @link http://smarty.php.net/manual/en/language.modifier.count.characters.php
+ * count_characters (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @param boolean include whitespace in the character count
+ * @return integer
+ */
+function tpl_modifier_count_characters($string, $include_spaces = false)
+{
+ if ($include_spaces)
+ {
+ return(strlen($string));
+ }
+
+ return preg_match_all("/[^\s]/",$string, $match);
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.count_paragraphs.php b/include/libs/template_lite/plugins/modifier.count_paragraphs.php
new file mode 100644
index 0000000..bf7cef9
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.count_paragraphs.php
@@ -0,0 +1,27 @@
+
+ * Name: count_paragraphs
+ * Purpose: count the number of paragraphs in a text
+ * @link http://smarty.php.net/manual/en/language.modifier.count.paragraphs.php
+ * count_paragraphs (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @return integer
+ */
+function tpl_modifier_count_paragraphs($string)
+{
+ // count \r or \n characters
+ return count(preg_split('/[\r\n]+/', $string));
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.count_sentences.php b/include/libs/template_lite/plugins/modifier.count_sentences.php
new file mode 100644
index 0000000..80fb6a0
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.count_sentences.php
@@ -0,0 +1,27 @@
+
+ * Name: count_sentences
+ * Purpose: count the number of sentences in a text
+ * @link http://smarty.php.net/manual/en/language.modifier.count.paragraphs.php
+ * count_sentences (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @return integer
+ */
+function tpl_modifier_count_sentences($string)
+{
+ // find periods with a word before but not after.
+ return preg_match_all('/[^\s]\.(?!\w)/', $string, $match);
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.count_words.php b/include/libs/template_lite/plugins/modifier.count_words.php
new file mode 100644
index 0000000..0e49474
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.count_words.php
@@ -0,0 +1,31 @@
+
+ * Name: count_words
+ * Purpose: count the number of words in a text
+ * @link http://smarty.php.net/manual/en/language.modifier.count.words.php
+ * count_words (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @return integer
+ */
+function tpl_modifier_count_words($string)
+{
+ // split text by ' ',\r,\n,\f,\t
+ $split_array = preg_split('/\s+/',$string);
+ // count matches that contain alphanumerics
+ $word_count = preg_grep('/[a-zA-Z0-9\\x80-\\xff]/', $split_array);
+
+ return count($word_count);
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.date.php b/include/libs/template_lite/plugins/modifier.date.php
new file mode 100644
index 0000000..9d2d628
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.date.php
@@ -0,0 +1,63 @@
+ 0)
+ {
+ return $time;
+ }
+ else
+ {
+ return time();
+ }
+ }
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.date_format.php b/include/libs/template_lite/plugins/modifier.date_format.php
new file mode 100644
index 0000000..762562e
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.date_format.php
@@ -0,0 +1,64 @@
+ 0)
+ {
+ return $time;
+ }
+ else
+ {
+ return time();
+ }
+ }
+}
+?>
diff --git a/include/libs/template_lite/plugins/modifier.debug_print_var.php b/include/libs/template_lite/plugins/modifier.debug_print_var.php
new file mode 100644
index 0000000..6ad7a52
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.debug_print_var.php
@@ -0,0 +1,54 @@
+Array (".count($var).")";
+ foreach ($var as $curr_key => $curr_val)
+ {
+ $return = tpl_modifier_debug_print_var($curr_val, $depth+1, $length);
+ $results .= ' \r'.str_repeat(' ', $depth*2)."$curr_key => $return";
+ }
+ return $results;
+ }
+ else if (is_object($var))
+ {
+ $object_vars = get_object_vars($var);
+ $results = "".get_class($var)." Object (".count($object_vars).") ";
+ foreach ($object_vars as $curr_key => $curr_val)
+ {
+ $return = tpl_modifier_debug_print_var($curr_val, $depth+1, $length);
+ $results .= ' \r'.str_repeat(' ', $depth*2)."$curr_key => $return";
+ }
+ return $results;
+ }
+ else
+ {
+ if (empty($var) && $var != "0")
+ {
+ return 'empty ';
+ }
+ if (strlen($var) > $length )
+ {
+ $results = substr($var, 0, $length-3).'...';
+ }
+ else
+ {
+ $results = $var;
+ }
+ $results = preg_replace("![\r\t\n]!", " ", $results);
+ $results = htmlspecialchars($results);
+ return $results;
+ }
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.default.php b/include/libs/template_lite/plugins/modifier.default.php
new file mode 100644
index 0000000..0dcaf6d
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.default.php
@@ -0,0 +1,22 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.escape.php b/include/libs/template_lite/plugins/modifier.escape.php
new file mode 100644
index 0000000..d04c408
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.escape.php
@@ -0,0 +1,102 @@
+
+ * Name: escape
+ * Purpose: Escape the string according to escapement type
+ * @link http://smarty.php.net/manual/en/language.modifier.escape.php
+ * escape (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @param html|htmlall|url|quotes|hex|hexentity|javascript
+ * @return string
+ */
+function tpl_modifier_escape($string, $esc_type = 'html', $char_set = 'ISO-8859-1', $double_encode = true)
+{
+ switch ($esc_type)
+ {
+ case 'html':
+ if (version_compare(PHP_VERSION, '5.2.3') === 1)
+ return htmlspecialchars($string, ENT_QUOTES, $char_set, $double_encode);
+ else
+ return htmlspecialchars($string, ENT_QUOTES, $char_set);
+
+ case 'htmlall':
+ if (version_compare(PHP_VERSION, '5.2.3') === 1)
+ return htmlentities($string, ENT_QUOTES, $char_set, $double_encode);
+ else
+ return htmlentities($string, ENT_QUOTES, $char_set);
+
+ case 'url':
+ return rawurlencode($string);
+
+ case 'urlpathinfo':
+ return str_replace('%2F','/',rawurlencode($string));
+
+ case 'quotes':
+ // escape unescaped single quotes
+ return preg_replace("%(?'\\\\',"'"=>"\\'",'"'=>'\\"',"\r"=>'\\r',"\n"=>'\\n',''=>'<\/'));
+
+ case 'mail':
+ // safe way to display e-mail address on a web page
+ return str_replace(array('@', '.'),array(' [AT] ', ' [DOT] '), $string);
+
+ case 'nonstd':
+ // escape non-standard chars, such as ms document quotes
+ $_res = '';
+ for($_i = 0, $_len = strlen($string); $_i < $_len; $_i++)
+ {
+ $_ord = ord(substr($string, $_i, 1));
+ // non-standard char, escape it
+ if($_ord >= 126)
+ {
+ $_res .= '' . $_ord . ';';
+ }
+ else
+ {
+ $_res .= substr($string, $_i, 1);
+ }
+ }
+ return $_res;
+
+ default:
+ return $string;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.indent.php b/include/libs/template_lite/plugins/modifier.indent.php
new file mode 100644
index 0000000..46661c4
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.indent.php
@@ -0,0 +1,28 @@
+
+ * Name: indent
+ * Purpose: indent lines of text
+ * @link http://smarty.php.net/manual/en/language.modifier.indent.php
+ * indent (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @param integer
+ * @param string
+ * @return string
+ */
+function tpl_modifier_indent($string,$chars=4,$char=" ")
+{
+ return preg_replace('!^!m',str_repeat($char,$chars),$string);
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.lower.php b/include/libs/template_lite/plugins/modifier.lower.php
new file mode 100644
index 0000000..b59e74f
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.lower.php
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.regex_replace.php b/include/libs/template_lite/plugins/modifier.regex_replace.php
new file mode 100644
index 0000000..d988429
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.regex_replace.php
@@ -0,0 +1,33 @@
+
+ * Name: regex_replace
+ * Purpose: regular expression search/replace
+ * @link http://smarty.php.net/manual/en/language.modifier.regex.replace.php
+ * regex_replace (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @param string|array
+ * @param string|array
+ * @return string
+ */
+function tpl_modifier_regex_replace($string, $search, $replace)
+{
+ if (preg_match('!([a-zA-Z\s]+)$!s', $search, $match) && (strpos($match[1], 'e') !== false))
+ {
+ /* remove eval-modifier from $search */
+ $search = substr($search, 0, -strlen($match[1])) . preg_replace('![e\s]+!', '', $match[1]);
+ }
+ return preg_replace($search, $replace, $string);
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.replace.php b/include/libs/template_lite/plugins/modifier.replace.php
new file mode 100644
index 0000000..059fa70
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.replace.php
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.spacify.php b/include/libs/template_lite/plugins/modifier.spacify.php
new file mode 100644
index 0000000..3b72a17
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.spacify.php
@@ -0,0 +1,27 @@
+
+ * Name: spacify
+ * Purpose: add spaces between characters in a string
+ * @link http://smarty.php.net/manual/en/language.modifier.spacify.php
+ * spacify (Smarty online manual)
+ * @author Monte Ohrt
+ * @param string
+ * @param string
+ * @return string
+ */
+function tpl_modifier_spacify($string, $spacify_char = ' ')
+{
+ return implode($spacify_char, preg_split('//', $string, -1, PREG_SPLIT_NO_EMPTY));
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/modifier.string_format.php b/include/libs/template_lite/plugins/modifier.string_format.php
new file mode 100644
index 0000000..3d777b1
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.string_format.php
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.strip.php b/include/libs/template_lite/plugins/modifier.strip.php
new file mode 100644
index 0000000..8dcc118
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.strip.php
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.truncate.php b/include/libs/template_lite/plugins/modifier.truncate.php
new file mode 100644
index 0000000..24b8025
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.truncate.php
@@ -0,0 +1,34 @@
+ $length)
+ {
+ $length -= strlen($etc);
+ if (!$break_words)
+ {
+ $string = preg_replace('/\s+?(\S+)?$/', '', substr($string, 0, $length+1));
+ }
+ return substr($string, 0, $length).$etc;
+ }
+ else
+ {
+ return $string;
+ }
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/modifier.upper.php b/include/libs/template_lite/plugins/modifier.upper.php
new file mode 100644
index 0000000..d077830
--- /dev/null
+++ b/include/libs/template_lite/plugins/modifier.upper.php
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/outputfilter.gzip.php b/include/libs/template_lite/plugins/outputfilter.gzip.php
new file mode 100644
index 0000000..7ef473b
--- /dev/null
+++ b/include/libs/template_lite/plugins/outputfilter.gzip.php
@@ -0,0 +1,61 @@
+enable_gzip = 0 output is not compressed $template_object->enable_gzip = 1 output is compressed
+ */
+
+function template_outputfilter_gzip($tpl_source, &$template_object)
+{
+ static $_tpl_saved = '';
+
+ $gzipped = 0;
+ if($template_object->enable_gzip)
+ {
+ if(extension_loaded("zlib") && !get_cfg_var('zlib.output_compression') && !$template_object->cache && (strstr($_SERVER["HTTP_ACCEPT_ENCODING"],"gzip") || $template_object->force_compression))
+ {
+ $_tpl_saved .= $tpl_source . "\n\n\n";
+ $tpl_source = "";
+
+ if($template_object->send_now == 1)
+ {
+ $gzipped = 1;
+ $tpl_source = gzencode($_tpl_saved, $template_object->compression_level);
+ $_tpl_saved = "";
+ }
+ }
+ }
+ else
+ {
+ if(!$template_object->caching && !get_cfg_var('zlib.output_compression'))
+ {
+ $_tpl_saved .= $tpl_source."\n\n\n";
+ $tpl_source = "";
+
+ if($template_object->send_now == 1)
+ {
+ $tpl_source = $_tpl_saved;
+ $_tpl_saved = "";
+ }
+ }
+ }
+
+ if($template_object->send_now == 1 && $template_object->enable_gzip == 1)
+ {
+ if($gzipped == 1)
+ {
+ header("Content-Encoding: gzip");
+ header("Content-Length: " . strlen($tpl_source));
+ }
+ }
+
+ return $tpl_source;
+}
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/outputfilter.trimwhitespace.php b/include/libs/template_lite/plugins/outputfilter.trimwhitespace.php
new file mode 100644
index 0000000..38a9bff
--- /dev/null
+++ b/include/libs/template_lite/plugins/outputfilter.trimwhitespace.php
@@ -0,0 +1,81 @@
+
+ * Type: outputfilter
+ * Name: trimwhitespace
+ * Date: Jan 25, 2003
+ * Purpose: trim leading white space and blank lines from
+ * template source after it gets interpreted, cleaning
+ * up code and saving bandwidth. Does not affect
+ * <> and blocks.
+ * Install: Drop into the plugin directory, call
+ * $template_object->load_filter('output','trimwhitespace');
+ * from application.
+ * @author Monte Ohrt
+ * @author Contributions from Lars Noschinski
+ * @version 1.3
+ * @param string
+ * @param Smarty
+ */
+
+function template_outputfilter_trimwhitespace($tpl_source, &$template_object)
+{
+ // Pull out the script blocks
+ preg_match_all("!!is", $tpl_source, $match);
+ $_script_blocks = $match[0];
+ $tpl_source = preg_replace("!!is",
+ '@@@TEMPLATELITE:TRIM:SCRIPT@@@', $tpl_source);
+
+ // Pull out the pre blocks
+ preg_match_all("!.*? !is", $tpl_source, $match);
+ $_pre_blocks = $match[0];
+ $tpl_source = preg_replace("!.*? !is",
+ '@@@TEMPLATELITE:TRIM:PRE@@@', $tpl_source);
+
+ // Pull out the textarea blocks
+ preg_match_all("!!is", $tpl_source, $match);
+ $_textarea_blocks = $match[0];
+ $tpl_source = preg_replace("!!is",
+ '@@@TEMPLATELITE:TRIM:TEXTAREA@@@', $tpl_source);
+
+ // remove all leading spaces, tabs and carriage returns NOT
+ // preceeded by a php close tag.
+ $tpl_source = trim(preg_replace('/((?)\n)[\s]+/m', '\1', $tpl_source));
+
+ // replace script blocks
+ template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:SCRIPT@@@",$_script_blocks, $tpl_source);
+
+ // replace pre blocks
+ template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:PRE@@@",$_pre_blocks, $tpl_source);
+
+ // replace textarea blocks
+ template_outputfilter_trimwhitespace_replace("@@@TEMPLATELITE:TRIM:TEXTAREA@@@",$_textarea_blocks, $tpl_source);
+
+ return $tpl_source;
+}
+
+function template_outputfilter_trimwhitespace_replace($search_str, $replace, &$subject) {
+ $_len = strlen($search_str);
+ $_pos = 0;
+ for ($_i=0, $_count=count($replace); $_i<$_count; $_i++)
+ {
+ if (($_pos=strpos($subject, $search_str, $_pos))!==false)
+ {
+ $subject = substr_replace($subject, $replace[$_i], $_pos, $_len);
+ }
+ else
+ {
+ break;
+ }
+ }
+}
+
+?>
diff --git a/include/libs/template_lite/plugins/postfilter.showtemplatevars.php b/include/libs/template_lite/plugins/postfilter.showtemplatevars.php
new file mode 100644
index 0000000..108a067
--- /dev/null
+++ b/include/libs/template_lite/plugins/postfilter.showtemplatevars.php
@@ -0,0 +1,16 @@
+\n_vars); ?>\n" . $compiled;
+ return $compiled;
+ }
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/prefilter.jstrip.php b/include/libs/template_lite/plugins/prefilter.jstrip.php
new file mode 100644
index 0000000..9b24cb2
--- /dev/null
+++ b/include/libs/template_lite/plugins/prefilter.jstrip.php
@@ -0,0 +1,130 @@
+";
+ }
+
+ //line break
+ if ($c[$i]=="\n" OR $c[$i]=="\r")
+ {
+ //is the current line finished ?
+ // ")" and "}" is not OK ! (var x=function a() {}.......var )
+ $finishers=array(";","{","(",",","\n",":");
+ if (in_array($last,$finishers))
+ {
+ $s=false;
+ }
+ }
+
+ //a space ! can we cut it ?
+ if ($c[$i]==" " OR $c[$i]=="\t")
+ {
+ $cutme=array(" ","\t","}","{",")","(","[","]","<",">","=",";","+","-","/","*","\n",":","&");
+ if (in_array($c[$i-1],$cutme) OR in_array($c[$i+1],$cutme))
+ {
+ $s=false;
+ }
+ }
+ //todo : rename vars/functions !!
+ }
+ }
+ //save the character
+ if ($s AND $comment==0)
+ {
+ $o.=$c[$i];
+ $last=$c[$i];
+ }
+ }
+
+ if ($literal)
+ {
+ return "{literal}".$o."{/literal}";
+ }
+ else
+ {
+ return $o;
+ }
+}
+
+?>?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/prefilter.showinfoheader.php b/include/libs/template_lite/plugins/prefilter.showinfoheader.php
new file mode 100644
index 0000000..11f96a3
--- /dev/null
+++ b/include/libs/template_lite/plugins/prefilter.showinfoheader.php
@@ -0,0 +1,23 @@
+
+ * -------------------------------------------------------------
+ */
+
+ function template_prefilter_showinfoheader($tpl_source, &$template_object)
+ {
+ return ''."\n\n".$tpl_source;
+ }
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/plugins/shared.escape_chars.php b/include/libs/template_lite/plugins/shared.escape_chars.php
new file mode 100644
index 0000000..777884e
--- /dev/null
+++ b/include/libs/template_lite/plugins/shared.escape_chars.php
@@ -0,0 +1,18 @@
+
diff --git a/include/libs/template_lite/plugins/shared.make_timestamp.php b/include/libs/template_lite/plugins/shared.make_timestamp.php
new file mode 100644
index 0000000..7bc7f1e
--- /dev/null
+++ b/include/libs/template_lite/plugins/shared.make_timestamp.php
@@ -0,0 +1,40 @@
+ 0)
+ {
+ return $time;
+ }
+ else
+ {
+ return time();
+ }
+}
+
+?>
diff --git a/include/libs/template_lite/tests/parser.php b/include/libs/template_lite/tests/parser.php
new file mode 100644
index 0000000..7023313
--- /dev/null
+++ b/include/libs/template_lite/tests/parser.php
@@ -0,0 +1,51 @@
+debug[] = array('Processing string', $content);
+ return parent::processString($content);
+ }
+
+ public function processModifier($name, $content, $arguments, $map_array)
+ {
+ $this->debug[] = array('Processing modifier', $name, $content, $arguments);
+ return parent::processModifier($name, $content, $arguments, $map_array);
+ }
+
+ public function processVariable($name)
+ {
+ $this->debug[] = array('Processing variable', $name);
+ return parent::processVariable($name);
+ }
+
+ public function testArgs($args)
+ {
+ return $this->parseArguments($args);
+ }
+}
+
+$test = new Template_Tester;
+
+$args = 'truc="miam $blu\' oh" miam="ah `$bla|blu`" bla=$bla|blu autre=$a|bb|cat:$miam|escape uh=bla::blou()';
+
+print_r($test->testArgs($args));
+
+foreach (token_get_all('') as $line)
+{
+ echo "\n";
+ if (is_array($line))
+ {
+ echo token_name($line[0]) . ": ";
+ echo $line[1];
+ echo "\t";
+ }
+ echo str_replace("\n", "", print_r($line, true));
+}
+
+?>
\ No newline at end of file
diff --git a/include/libs/template_lite/tests/tokenparser.php b/include/libs/template_lite/tests/tokenparser.php
new file mode 100644
index 0000000..0f6f469
--- /dev/null
+++ b/include/libs/template_lite/tests/tokenparser.php
@@ -0,0 +1,46 @@
+test($foo)|miam:"bla blu blou $t"|escape:Truc::getInstance()->miam( $foo )';
+$t = '\'miam coucou c"est marrant `$blu`s oh\'';
+//$t = 'foo123($foo,$foo->bar(),"foo")';
+//$t = '$foo|bar';
+
+$result = $parser->parseArgumentContent($t);
+var_dump($result);
+
+exit;
+
+$args = 'first="Bla::`$blou`" truc="miam coucou c\'est marrant $blu\' oh" miam="ah `$bla|blu`" bla=$bla|blu autre=$a|bb|cat:$miam|escape uh=bla::blou()';
+
+echo '';
+
+print_r($parser->parseArguments($args));
+$parser->parseTokens($args);
+
+/*
+$content = '
+
+{literal}
+
+Miam
+
+function ()
+{
+}
+
+{/literal}
+
+
+
+';
+
+$tp = new Template_Parser;
+echo $tp->Parse($content);*/
+
+?>
\ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..3bbf58a
--- /dev/null
+++ b/index.php
@@ -0,0 +1,13 @@
+='))
+{
+ die('PHP 5.4.0 ou supérieur est nécessaire au fonctionnement de Garradin.');
+}
+
+header('Location: www/');
diff --git a/plugins/index.html b/plugins/index.html
new file mode 100644
index 0000000..9a31a28
--- /dev/null
+++ b/plugins/index.html
@@ -0,0 +1 @@
+404 Not Found Not Found The requested URL was not found on this server.
\ No newline at end of file
diff --git a/templates/admin/_foot.tpl b/templates/admin/_foot.tpl
new file mode 100644
index 0000000..6c65910
--- /dev/null
+++ b/templates/admin/_foot.tpl
@@ -0,0 +1,19 @@
+
+
+
+
+